From f3bea31e9185edadc5c5e6df280711d271679597 Mon Sep 17 00:00:00 2001 From: xianyi Date: Tue, 25 Nov 2025 18:16:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hisApi.ts | 23 ++- src/main.tsx | 4 + src/pages/U2/u2.tsx | 2 + src/pages/U4/u4.tsx | 5 +- src/pages/UI6/UI6.tsx | 102 +++++----- src/pages/UI7/UI7.css | 2 +- src/pages/UI7/UI7.tsx | 422 +++++++++++++++-------------------------- src/pages/UI8/UI8.css | 21 +- src/pages/UI8/UI8.tsx | 381 ++++++++++++++----------------------- src/pages/UI9/UI9.tsx | 32 +++- src/utils/pdfWorker.ts | 27 +++ 11 files changed, 439 insertions(+), 582 deletions(-) create mode 100644 src/utils/pdfWorker.ts diff --git a/src/api/hisApi.ts b/src/api/hisApi.ts index 73aa9e0..c73eac8 100644 --- a/src/api/hisApi.ts +++ b/src/api/hisApi.ts @@ -84,6 +84,12 @@ export interface PackagItemDetailResponse { // PDF响应 export interface PdfResponse { + list_pdf_url: string[]; + message: string | null; +} + +export interface DaojiandanPdfResponse { + exam_id: number; pdf_url: string; message: string | null; } @@ -196,14 +202,14 @@ export async function getPackagItemDetail( ): Promise> { const response = await axiosInstance.post< ApiResponse - >("optional-item-list", { + >("package-item-list", { id_no, }); return response.data; } /** - * 5. 获取体检知情同意书PDF + * 5. 获取体检知情同意书PDFurlList */ export async function getTongyishuPdf( exam_id: number @@ -217,6 +223,15 @@ export async function getTongyishuPdf( return response.data; } +/** + * 5.5. 拿取体检知情同意书PDF + */ +export async function getTongyishuPdfFile( + pdf_url: string +): Promise> { + const response = await axiosInstance.get>(pdf_url); + return response.data; +} /** * 6. 体检知情同意书签名,返回生成的知情同意书PDF * @param exam_id 体检ID @@ -247,8 +262,8 @@ export async function submitTongyishuSign( */ export async function getDaojiandanPdf( exam_id: number -): Promise> { - const response = await axiosInstance.post>( +): Promise> { + const response = await axiosInstance.post>( "daojiandan-get", { exam_id, diff --git a/src/main.tsx b/src/main.tsx index 25d1be2..9003ac0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -66,6 +66,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { HashRouter } from "react-router-dom"; import App from "./App"; +import { initPdfWorker } from "./utils/pdfWorker"; + +// 在应用启动时初始化 PDF Worker,确保只初始化一次 +initPdfWorker(); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/pages/U2/u2.tsx b/src/pages/U2/u2.tsx index 753e7b2..50ca62e 100644 --- a/src/pages/U2/u2.tsx +++ b/src/pages/U2/u2.tsx @@ -28,6 +28,8 @@ const U2: React.FC = () => { .then((res) => { if (res.Status === 200) { setPatientInfo(res.Data); + localStorage.setItem("name", res.Data.name); + localStorage.setItem("gender", res.Data.gender_name); } else { alert(`获取用户信息失败: ${res.Message}`); } diff --git a/src/pages/U4/u4.tsx b/src/pages/U4/u4.tsx index ce82cb2..5966de0 100644 --- a/src/pages/U4/u4.tsx +++ b/src/pages/U4/u4.tsx @@ -11,6 +11,7 @@ interface testType { title: string; desc: string; taboo: string; + exam_id: number; } const U4: React.FC = () => { @@ -32,6 +33,7 @@ const U4: React.FC = () => { title: it.combination_name, desc: "", taboo: "", + exam_id: it.physical_exam_id, })); setTest(items); } else { @@ -43,6 +45,7 @@ const U4: React.FC = () => { React.useEffect(() => { console.log("选择的项目", selectedId); + }, [selectedId]); return (
@@ -54,7 +57,7 @@ const U4: React.FC = () => {
setSelectedId(t.id)} + onClick={() => {setSelectedId(t.id); localStorage.setItem("selectedExamId", t.exam_id.toString())}} >
{ + + // ! + localStorage.setItem("selectedExamId", "100030906"); + const navigate = useNavigate(); const handleBack = () => { @@ -16,49 +21,48 @@ const UI6: React.FC = () => { const handleConfirm = () => { navigate("/UI7"); }; - - const testData = [ - { - "department": "B超科室", - "project": ["甲状腺B超", "腹部B超" , "乳腺B超"] - }, - { - "department": "血常规科室", - "project": ["血常规", "血型"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图"] - }, - { - "department": "心电图科室", - "project": ["心电图", "心电图","心电图", "心电图","心电图", "心电图","心电图", "心电图","心电图", "心电图"] + + const [ListData, setListData] = useState([]); + const [PackageInfo, setPackageInfo] = useState({}); + useEffect(() => { + getListData(); + }, []); + + + const getListData = async () => { + const id_no = localStorage.getItem("lastIdCardNo"); + if (!id_no) { + alert("请先输入身份证号"); + return; } - ] + const res = await getPackagItemDetail(id_no as string); + if (res.Status === 200) { + // 处理数据:将 project_id 和 project_name 字符串分离为数组 + const processedData = res.Data.listPackDetail.map((item: any) => { + // 将 project_id 字符串按中文顿号分割为数组 + const project_ids = item.project_id + ? item.project_id.split("、").map((id: string) => id.trim()).filter((id: string) => id) + : []; + + // 将 project_name 字符串按中文顿号分割为数组 + const project_names = item.project_name + ? item.project_name.split("、").map((name: string) => name.trim()).filter((name: string) => name) + : []; + + return { + ...item, + project_ids, + project_names, + }; + }); + + setListData(processedData); + setPackageInfo(res.Data.packagItemInfo); + } else { + alert(`获取列表数据失败: ${res.Message}`); + } + }; + return (
@@ -68,10 +72,10 @@ const UI6: React.FC = () => { - 张哈哈女士定制套餐 + {localStorage.getItem("name")}{localStorage.getItem("gender") === "男" ? "先生" : "女士"}定制套餐

- 已帮您成功预约2022-02-219:00-9:30的体检,以下是体检套餐详情和价格。 + 已帮您成功预约 {PackageInfo.appointment_datetime} 的体检,以下是体检套餐详情和价格。
@@ -83,11 +87,11 @@ const UI6: React.FC = () => { - {testData.map((item, index) => ( + {ListData.map((item, index) => ( - {item.department} + {item.department_name} - {item.project.map((project, pIndex) => ( + {item.project_names.map((project: string, pIndex: number) => (
{project}
@@ -101,7 +105,7 @@ const UI6: React.FC = () => {
- +
diff --git a/src/pages/UI7/UI7.css b/src/pages/UI7/UI7.css index 9c8548e..bac3758 100644 --- a/src/pages/UI7/UI7.css +++ b/src/pages/UI7/UI7.css @@ -1,5 +1,5 @@ .ui7-text-wrapper { - height: 623px; + height: 1080px; width: 975px; border: 2px solid #000; border-radius: 30px; diff --git a/src/pages/UI7/UI7.tsx b/src/pages/UI7/UI7.tsx index 8a95533..ad80149 100644 --- a/src/pages/UI7/UI7.tsx +++ b/src/pages/UI7/UI7.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useRef } from "react"; -import { pdfjs } from "react-pdf"; +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { Document, Page } from "react-pdf"; import "./UI7.css"; import "../../assets/css/basic.css"; import { useNavigate } from "react-router-dom"; @@ -8,115 +8,155 @@ import ConfirmButton from "../../components/ConfirmButton"; import DecorLine from "../../components/DecorLine"; import WaitButton from "../../components/WaitButton"; -// @ts-ignore - Vite 会处理 ?raw 导入 -import testPdfBase64Raw from "../../assets/testPdfBase64?raw"; -const testPdfBase64 = testPdfBase64Raw.trim(); +import { getTongyishuPdf } from "../../api/hisApi"; -// 配置 PDF.js worker -const workerPath = new URL("pdf.worker.min.js", document.baseURI).href; -pdfjs.GlobalWorkerOptions.workerSrc = workerPath; - -if (window.electronAPI) { - window.electronAPI.log("info", `[UI7] PDF Worker 路径: ${workerPath}`); -} +// 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染 +const PdfRenderer = React.memo<{ + pdfFiles: Uint8Array[]; + loading: boolean; + error: string; + pageCounts: Record; + onPageCountUpdate: (index: number, numPages: number) => void; + loadMessage: string; +}>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, loadMessage }) => { + return ( + <> + {loading && ( +
+ {loadMessage} +
+ )} + {error && ( +
+ {error} +
+ )} + {!loading && + !error && + pdfFiles.map((fileData, index) => ( +
+ onPageCountUpdate(index, numPages)} + > + {Array.from( + { length: pageCounts[index] || 0 }, + (_, pageIndex) => ( + + ) + )} + +
+ ))} + + ); +}); +PdfRenderer.displayName = "PdfRenderer"; const UI7: React.FC = () => { const navigate = useNavigate(); + const hasFetchedRef = useRef(false); const [countdown, setCountdown] = useState(5); const [showWaitButton, setShowWaitButton] = useState(true); - const canvasRef = useRef(null); - const [isDrawing, setIsDrawing] = useState(false); - const dprRef = useRef(1); - const lastPointRef = useRef<{ x: number; y: number } | null>(null); - - // PDF 文本提取相关状态 - const [pdfText, setPdfText] = useState(""); + const [pdfFiles, setPdfFiles] = useState([]); + const [pageCounts, setPageCounts] = + useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + const [loadMessage, setLoadMessage] = useState("PDF加载中..."); - // 从 PDF 中提取文本 - useEffect(() => { - const extractTextFromPdf = async () => { - try { - setLoading(true); - window.electronAPI?.log("info", "[UI7] 开始从 PDF 提取文本"); - - // 清理 base64 字符串(移除可能的换行和空格) - const cleanBase64 = testPdfBase64.replace(/\s/g, ""); - window.electronAPI?.log("info", `[UI7] Base64 长度: ${cleanBase64.length}`); - - // 将 base64 转换为 Uint8Array - const binaryString = atob(cleanBase64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - window.electronAPI?.log("info", `[UI7] 转换为 Uint8Array,长度: ${bytes.length}`); - - // 加载 PDF 文档(使用 Uint8Array) - const loadingTask = pdfjs.getDocument({ data: bytes }); - const pdf = await loadingTask.promise; - - window.electronAPI?.log("info", `[UI7] PDF 加载成功,共 ${pdf.numPages} 页`); - - // 提取所有页面的文本 - let allText = ""; - for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { - const page = await pdf.getPage(pageNum); - const textContent = await page.getTextContent(); - - // 将文本项合并为字符串,保留换行信息 - let pageText = ""; - let lastY = null; - - for (let i = 0; i < textContent.items.length; i++) { - const item = textContent.items[i] as any; - const currentY = item.transform ? item.transform[5] : null; // Y 坐标 - - // 如果 Y 坐标变化较大(超过行高的一半),认为是新行 - if (lastY !== null && currentY !== null && Math.abs(currentY - lastY) > 5) { - pageText += "\n"; - } - - pageText += item.str; - - // 如果当前项后面有空格标记,添加空格 - if (item.hasEOL) { - pageText += "\n"; - } else if (i < textContent.items.length - 1) { - const nextItem = textContent.items[i + 1] as any; - const nextX = nextItem.transform ? nextItem.transform[4] : null; - const currentX = item.transform ? item.transform[4] : null; - const currentWidth = item.width || 0; - - // 如果下一个文本项距离较远,添加空格 - if (currentX !== null && nextX !== null && (nextX - (currentX + currentWidth)) > 2) { - pageText += " "; - } - } - - lastY = currentY; + + // 辅助函数:从 URL 获取 PDF 的 Uint8Array + const fetchPdfBytes = async (url: string) => { + try { + if (window.electronAPI?.fetchPdf) { + window.electronAPI.log("info", `[UI7] 通过 Electron 获取 PDF: ${url}`); + const result = await window.electronAPI.fetchPdf(url); + if (result.success && result.data) { + const cleanBase64 = result.data.replace(/\s/g, ""); + const binaryString = atob(cleanBase64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); } - - allText += pageText + "\n\n"; + return bytes; + } + throw new Error(result.error || "fetchPdf 返回失败"); + } + + window.electronAPI?.log("info", `[UI7] 通过 fetch 获取 PDF: ${url}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`网络请求失败: ${response.status}`); + } + const arrayBuffer = await response.arrayBuffer(); + return new Uint8Array(arrayBuffer); + } catch (error) { + throw new Error(`获取 PDF 失败: ${(error as Error).message}`); + } + }; + + // 从多份 PDF 中获取数据 + useEffect(() => { + if (hasFetchedRef.current) { + window.electronAPI?.log("info", "[UI7] 已获取过 PDF,跳过重复请求"); + return; + } + hasFetchedRef.current = true; + + const fetchPdfs = async () => { + try { + const res = await getTongyishuPdf( + Number(localStorage.getItem("selectedExamId")) + ); + if (res.Status !== 200) { + alert(`获取PDF数据失败: ${res.Message}`); + setLoadMessage(res.Message || "获取PDF数据失败"); + return; + } + + const pdfUrls: string[] = res.Data?.list_pdf_url || []; + if (!pdfUrls.length) { + throw new Error("未获取到任何 PDF 链接"); + } + + setLoading(true); + setError(""); + setPdfFiles([]); + window.electronAPI?.log("info", "[UI7] 开始获取 PDF 文件"); + + const files: Uint8Array[] = []; + for (let idx = 0; idx < pdfUrls.length; idx++) { + const url = pdfUrls[idx]; + window.electronAPI?.log( + "info", + `[UI7] 下载第 ${idx + 1} 份 PDF: ${url}` + ); + const pdfBytes = await fetchPdfBytes(url); + files.push(pdfBytes); } - window.electronAPI?.log("info", `[UI7] PDF 文本提取完成,长度: ${allText.length}`); - setPdfText(allText.trim()); + setPdfFiles(files); setLoading(false); setError(""); } catch (err) { - const errorMsg = `PDF文本提取失败: ${err}`; + const errorMsg = `PDF获取失败: ${err}`; console.error(errorMsg); window.electronAPI?.log("error", `[UI7] ${errorMsg}`); - setError("PDF 文本提取失败,请检查文件"); + setError("PDF 获取失败,请检查文件"); setLoading(false); } }; - extractTextFromPdf(); + fetchPdfs(); }, []); useEffect(() => { @@ -130,160 +170,21 @@ const UI7: React.FC = () => { } }, [countdown]); - useEffect(() => { - const initCanvas = () => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext("2d", { - willReadFrequently: false, - alpha: true - }); - if (!ctx) return; - - // 获取 canvas 的显示尺寸 - const rect = canvas.getBoundingClientRect(); - // 使用更高的缩放倍数(3倍)来提高分辨率 - const scale = 3; - dprRef.current = scale; - - // 设置 canvas 内部尺寸(高分辨率,3倍) - canvas.width = rect.width * scale; - canvas.height = rect.height * scale; - - // 缩放上下文以匹配显示尺寸 - ctx.scale(scale, scale); - - // 设置 canvas 的 CSS 尺寸为显示尺寸 - canvas.style.width = `${rect.width}px`; - canvas.style.height = `${rect.height}px`; - - // 设置绘制样式(启用抗锯齿,优化参数) - ctx.strokeStyle = "#000000"; - ctx.lineWidth = 2; - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = "high"; - ctx.globalCompositeOperation = "source-over"; - }; - - // 延迟初始化以确保 DOM 完全渲染 - const timer = setTimeout(initCanvas, 100); - return () => clearTimeout(timer); - }, []); - - const getCoordinates = (e: React.MouseEvent | React.TouchEvent) => { - const canvas = canvasRef.current; - if (!canvas) return { x: 0, y: 0 }; - - const rect = canvas.getBoundingClientRect(); - if ("touches" in e) { - return { - x: e.touches[0].clientX - rect.left, - y: e.touches[0].clientY - rect.top, - }; - } else { - return { - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }; - } - }; - - const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - const { x, y } = getCoordinates(e); - - // 记录起始点 - lastPointRef.current = { x, y }; - - ctx.beginPath(); - ctx.moveTo(x, y); - setIsDrawing(true); - }; - - const draw = (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); - if (!isDrawing || !lastPointRef.current) return; - - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - const { x, y } = getCoordinates(e); - const lastPoint = lastPointRef.current; - - // 使用二次贝塞尔曲线平滑绘制 - const midX = (lastPoint.x + x) / 2; - const midY = (lastPoint.y + y) / 2; - - ctx.quadraticCurveTo(lastPoint.x, lastPoint.y, midX, midY); - ctx.stroke(); - - // 更新最后一个点 - lastPointRef.current = { x, y }; - }; - - const stopDrawing = () => { - setIsDrawing(false); - lastPointRef.current = null; - }; - - const clearCanvas = () => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - // 清除整个 canvas(使用显示坐标系统) - const rect = canvas.getBoundingClientRect(); - ctx.clearRect(0, 0, rect.width, rect.height); - - // 重新初始化绘制样式 - ctx.strokeStyle = "#000000"; - ctx.lineWidth = 2; - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - }; - - const handleBack = () => { + const handleBack = useCallback(() => { navigate(-1); - }; - - const handleConfirm = () => { - const canvas = canvasRef.current; - if (!canvas) { - // navigate("/UI7"); - alert("签名不能为空,请签名后再提交"); - return; - } - - // 下载签名图片 - canvas.toBlob((blob) => { - if (!blob) return; - - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `签名_${new Date().getTime()}.png`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }, "image/png"); + }, [navigate]); + const handleConfirm = useCallback(() => { navigate("/UI8"); - }; + }, [navigate]); + + // 稳定 PDF 页数更新回调 + const handlePageCountUpdate = useCallback((index: number, numPages: number) => { + setPageCounts((prev) => { + if (prev[index] === numPages) return prev; + return { ...prev, [index]: numPages }; + }); + }, []); return (
@@ -294,46 +195,23 @@ const UI7: React.FC = () => {
- {loading &&
PDF文本提取中...
} - {error &&
{error}
} - {!loading && !error && pdfText && ( - - {pdfText.split("\n").map((line, index) => ( - - {line} - {index < pdfText.split("\n").length - 1 &&
} -
- ))} -
- )} +
- 请阅读后在下方签名确认 - -
- - -
-
{showWaitButton ? ( ) : ( - + )}
diff --git a/src/pages/UI8/UI8.css b/src/pages/UI8/UI8.css index b02237d..9902407 100644 --- a/src/pages/UI8/UI8.css +++ b/src/pages/UI8/UI8.css @@ -77,20 +77,27 @@ border-bottom: none; } -/* PDF 展示容器 */ +/* PDF 展示容器 - 改为滚动显示模式 */ .ui8-pdf-container { - height: 1000px; + height: 1200px; + overflow-y: auto; + overflow-x: hidden; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; position: relative; - user-select: none; - touch-action: pan-y; /* 允许垂直触摸滑动 */ - -webkit-user-select: none; - -webkit-touch-callout: none; + /* 隐藏滚动条但保持滚动功能 */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 和 Edge */ + padding: 20px 10px; } +.ui8-pdf-container::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +/* PDF 页面包装器 - 不再需要,保留以防兼容性 */ .ui8-pdf-page-wrapper { display: flex; justify-content: center; @@ -100,8 +107,6 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); overflow: hidden; max-width: 100%; - max-height: 1200px; - transition: transform 0.3s ease-out, opacity 0.3s ease-out; } /* 向上滑动动画(下一页) */ diff --git a/src/pages/UI8/UI8.tsx b/src/pages/UI8/UI8.tsx index 9cb2756..4764171 100644 --- a/src/pages/UI8/UI8.tsx +++ b/src/pages/UI8/UI8.tsx @@ -1,5 +1,5 @@ -import React, { useState, useRef, useEffect, useCallback } from "react"; -import { Document, Page, pdfjs } from "react-pdf"; +import React, { useState, useEffect, useCallback } from "react"; +import { Document, Page } from "react-pdf"; import "react-pdf/dist/esm/Page/AnnotationLayer.css"; import "./UI8.css"; import "../../assets/css/basic.css"; @@ -8,72 +8,137 @@ import BackButton from "../../components/BackButton"; import ConfirmButton from "../../components/ConfirmButton"; import ui8A from "../../assets/ui8A.png"; import ui8B from "../../assets/ui8B.png"; -import testPdf from "../../assets/testPdf.pdf"; +import { getDaojiandanPdf } from "../../api/hisApi"; -// 配置 PDF.js worker -// 开发环境:Vite 会从根路径提供 public/pdf.worker.min.js -// 生产环境:Electron 需要相对于 index.html 的路径 -const workerPath = new URL("pdf.worker.min.js", document.baseURI).href; -pdfjs.GlobalWorkerOptions.workerSrc = workerPath; +// 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染 +const PdfRenderer = React.memo<{ + pdfFiles: string[]; + loading: boolean; + error: string; + pageCounts: Record; + onPageCountUpdate: (index: number, numPages: number) => void; + onDocumentLoadError: (error: Error) => void; +}>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, onDocumentLoadError }) => { + return ( + <> + {loading &&
PDF加载中...
} + {error &&
{error}
} -if (window.electronAPI) { - window.electronAPI.log("info", `PDF Worker 路径: ${workerPath}`); - window.electronAPI.log("info", `Base URI: ${document.baseURI}`); -} + {!loading && + !error && + pdfFiles.map((fileData, index) => ( +
+ onPageCountUpdate(index, numPages)} + onLoadError={onDocumentLoadError} + > + {Array.from({ length: pageCounts[index] || 0 }, (_, pageIndex) => ( +
+ +
+ ))} +
+
+ ))} + + ); +}); + +PdfRenderer.displayName = "PdfRenderer"; -// 使用本地 PDF 文件进行测试 -const PDF_URL = testPdf; const UI8: React.FC = () => { const navigate = useNavigate(); - const [numPages, setNumPages] = useState(0); - const [pageNumber, setPageNumber] = useState(1); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [isPrinting, setIsPrinting] = useState(false); - const [pdfData, setPdfData] = useState(null); - const [isAnimating, setIsAnimating] = useState(false); - const [animationDirection, setAnimationDirection] = useState<"up" | "down" | null>(null); - const scrollContainerRef = useRef(null); - const touchStartRef = useRef<{ x: number; y: number } | null>(null); + const [pdfFiles, setPdfFiles] = useState([]); + const [pageCounts, setPageCounts] = useState>({}); + const [originPdfUrls, setOriginPdfUrls] = useState([]); + + const getExamId = () => { + const storedId = localStorage.getItem("selectedExamId"); + return storedId || ""; + }; + + const arrayBufferToDataUrl = (arrayBuffer: ArrayBuffer) => { + return new Promise((resolve, reject) => { + const blob = new Blob([arrayBuffer], { type: "application/pdf" }); + const reader = new FileReader(); + reader.onloadend = () => { + if (typeof reader.result === "string") { + resolve(reader.result); + } else { + reject(new Error("PDF 转换失败")); + } + }; + reader.onerror = () => reject(new Error("PDF 读取失败")); + reader.readAsDataURL(blob); + }); + }; + + const fetchPdfDataUrl = async (url: string) => { + try { + if (window.electronAPI?.fetchPdf) { + window.electronAPI.log("info", `[UI8] 通过 Electron 获取 PDF: ${url}`); + const result = await window.electronAPI.fetchPdf(url); + if (result.success && result.data) { + const cleanBase64 = result.data.replace(/\s/g, ""); + return `data:application/pdf;base64,${cleanBase64}`; + } + throw new Error(result.error || "fetchPdf 返回失败"); + } + + window.electronAPI?.log("info", `[UI8] 通过 fetch 获取 PDF: ${url}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`网络请求失败: ${response.status}`); + } + const arrayBuffer = await response.arrayBuffer(); + return await arrayBufferToDataUrl(arrayBuffer); + } catch (err) { + throw new Error(`获取 PDF 失败: ${(err as Error).message}`); + } + }; // 加载PDF数据 useEffect(() => { const loadPdf = async () => { try { setLoading(true); - window.electronAPI?.log("info", `开始加载PDF: ${PDF_URL}`); - - // 本地文件直接使用,无需通过 fetchPdf - if (PDF_URL.startsWith("/") || PDF_URL.startsWith("blob:") || PDF_URL.includes("assets")) { - window.electronAPI?.log("info", `检测到本地PDF文件,直接加载: ${PDF_URL}`); - setPdfData(PDF_URL); - setLoading(false); - return; + setError(""); + setPdfFiles([]); + setPageCounts({}); + + const examId = getExamId(); + window.electronAPI?.log("info", `[UI8] 开始获取导检单 PDF,exam_id=${examId}`); + const res = await getDaojiandanPdf(parseInt(examId, 10)); + if (res.Status !== 200) { + throw new Error(res.Message || "获取导检单PDF失败"); } - // 远程 URL 需要通过 Electron 绕过 CORS - if (window.electronAPI?.fetchPdf) { - window.electronAPI.log("info", `通过Electron下载远程PDF: ${PDF_URL}`); - const result = await window.electronAPI.fetchPdf(PDF_URL); - if (result.success && result.data) { - // 将base64转换为data URL - window.electronAPI.log("info", `PDF下载成功,大小: ${result.data.length} bytes (base64)`); - setPdfData(`data:application/pdf;base64,${result.data}`); - } else { - const errorMsg = `PDF下载失败: ${result.error || "未知错误"}`; - window.electronAPI.log("error", errorMsg); - setError(errorMsg); - } - } else { - // 非Electron环境,直接使用URL - window.electronAPI?.log("warn", "非Electron环境,直接使用URL加载"); - setPdfData(PDF_URL); + const pdfUrl = res.Data?.pdf_url; + if (!pdfUrl) { + throw new Error("未获取到导检单 PDF"); } + + setOriginPdfUrls([pdfUrl]); + + window.electronAPI?.log("info", `[UI8] 下载导检单 PDF: ${pdfUrl}`); + const dataUrl = await fetchPdfDataUrl(pdfUrl); + setPdfFiles([dataUrl]); } catch (err) { - const errorMsg = `PDF fetch error: ${err}`; + const errorMsg = `导检单PDF获取失败: ${(err as Error).message || err}`; console.error(errorMsg); - window.electronAPI?.log("error", errorMsg); - setError("PDF加载失败,请检查网络连接"); + window.electronAPI?.log("error", `[UI8] ${errorMsg}`); + setError("PDF 获取失败,请稍后重试"); } finally { setLoading(false); } @@ -82,12 +147,12 @@ const UI8: React.FC = () => { loadPdf(); }, []); - const handleBack = () => { + const handleBack = useCallback(() => { navigate(-1); - }; + }, [navigate]); // 打印PDF功能 - const handleConfirm = async () => { + const handleConfirm = useCallback(async () => { if (!window.electronAPI?.printPdf) { const errorMsg = "打印功能不可用,请在 Electron 环境中运行"; window.electronAPI?.log("error", errorMsg); @@ -95,7 +160,8 @@ const UI8: React.FC = () => { return; } - if (!pdfData) { + const primaryPdf = pdfFiles[0] || originPdfUrls[0]; + if (!primaryPdf) { const errorMsg = "PDF 尚未加载完成,请稍候"; window.electronAPI?.log("warn", errorMsg); alert(errorMsg); @@ -104,9 +170,11 @@ const UI8: React.FC = () => { setIsPrinting(true); try { - // 本地文件直接传原始路径,远程文件传 base64 data URI - const printData = pdfData.startsWith("data:") ? pdfData : PDF_URL; - const dataType = pdfData.startsWith("data:") ? "base64数据" : "本地文件路径"; + const printData = + primaryPdf.startsWith("data:") && pdfFiles[0] + ? primaryPdf + : originPdfUrls[0]; + const dataType = printData.startsWith("data:") ? "base64数据" : "远程文件路径"; window.electronAPI.log("info", `开始打印PDF (${dataType}): ${printData.substring(0, 100)}...`); const result = await window.electronAPI.printPdf(printData); @@ -124,202 +192,41 @@ const UI8: React.FC = () => { alert("打印失败,请重试"); } finally { setIsPrinting(false); + navigate("/UI9"); } - }; + }, [originPdfUrls, pdfFiles]); - const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { - window.electronAPI?.log("info", `PDF渲染成功,共 ${numPages} 页`); - setNumPages(numPages); + const handlePageCountUpdate = useCallback((index: number, numPages: number) => { + window.electronAPI?.log("info", `[UI8] PDF渲染成功 (index=${index}),共 ${numPages} 页`); + setPageCounts((prev) => { + if (prev[index] === numPages) return prev; + return { ...prev, [index]: numPages }; + }); setLoading(false); setError(""); - }; + }, []); - const onDocumentLoadError = (error: Error) => { + const onDocumentLoadError = useCallback((error: Error) => { const errorMsg = `PDF渲染失败: ${error.message || error}`; console.error("PDF load error:", error); window.electronAPI?.log("error", errorMsg); - window.electronAPI?.log("error", `PDF数据: ${pdfData?.substring(0, 100)}...`); setError("PDF 加载失败,请检查网络连接"); setLoading(false); - }; - - const goToPrevPage = useCallback(() => { - if (isAnimating || pageNumber <= 1) return; - setIsAnimating(true); - setAnimationDirection("down"); - setTimeout(() => { - setPageNumber((prev) => Math.max(1, prev - 1)); - setIsAnimating(false); - setAnimationDirection(null); - }, 300); - }, [isAnimating, pageNumber]); - - const goToNextPage = useCallback(() => { - if (isAnimating || pageNumber >= numPages) return; - setIsAnimating(true); - setAnimationDirection("up"); - setTimeout(() => { - setPageNumber((prev) => Math.min(numPages, prev + 1)); - setIsAnimating(false); - setAnimationDirection(null); - }, 300); - }, [isAnimating, pageNumber, numPages]); - - // 监听触摸和鼠标事件实现上下滑动翻页 - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) return; - - let isDragging = false; - let startY = 0; - - // 触摸事件处理 - const handleTouchStart = (e: TouchEvent) => { - const touch = e.touches[0]; - touchStartRef.current = { - x: touch.clientX, - y: touch.clientY, - }; - window.electronAPI?.log("info", `触摸开始: (${touch.clientX}, ${touch.clientY})`); - }; - - const handleTouchEnd = (e: TouchEvent) => { - if (!touchStartRef.current) return; - - const touch = e.changedTouches[0]; - const deltaX = touch.clientX - touchStartRef.current.x; - const deltaY = touch.clientY - touchStartRef.current.y; - - window.electronAPI?.log("info", `触摸结束: delta(${deltaX}, ${deltaY})`); - - // 判断是否为有效滑动(至少50px,且垂直滑动大于水平滑动) - const minSwipeDistance = 50; - if (Math.abs(deltaY) > minSwipeDistance && Math.abs(deltaY) > Math.abs(deltaX)) { - if (deltaY < 0) { - // 向上滑动 = 下一页 - window.electronAPI?.log("info", "向上滑动 -> 下一页"); - goToNextPage(); - } else if (deltaY > 0) { - // 向下滑动 = 上一页 - window.electronAPI?.log("info", "向下滑动 -> 上一页"); - goToPrevPage(); - } - } - - touchStartRef.current = null; - }; - - const handleTouchCancel = () => { - touchStartRef.current = null; - }; - - // 鼠标事件处理(作为触摸的后备方案) - const handleMouseDown = (e: MouseEvent) => { - isDragging = true; - startY = e.clientY; - window.electronAPI?.log("info", `鼠标按下: (${e.clientX}, ${e.clientY})`); - }; - - const handleMouseMove = (e: MouseEvent) => { - if (!isDragging) return; - // 防止页面滚动 - e.preventDefault(); - }; - - const handleMouseUp = (e: MouseEvent) => { - if (!isDragging) return; - - const deltaY = e.clientY - startY; - window.electronAPI?.log("info", `鼠标释放: deltaY=${deltaY}`); - - // 判断是否为有效滑动(至少50px) - const minSwipeDistance = 50; - if (Math.abs(deltaY) > minSwipeDistance) { - if (deltaY < 0) { - // 向上拖动 = 下一页 - window.electronAPI?.log("info", "向上拖动 -> 下一页"); - goToNextPage(); - } else if (deltaY > 0) { - // 向下拖动 = 上一页 - window.electronAPI?.log("info", "向下拖动 -> 上一页"); - goToPrevPage(); - } - } - - isDragging = false; - }; - - // 注册触摸事件 - container.addEventListener("touchstart", handleTouchStart, { passive: true }); - container.addEventListener("touchend", handleTouchEnd, { passive: true }); - container.addEventListener("touchcancel", handleTouchCancel, { passive: true }); - - // 鼠标离开处理 - const handleMouseLeave = () => { - isDragging = false; - }; - - // 注册鼠标事件(作为后备) - container.addEventListener("mousedown", handleMouseDown); - container.addEventListener("mousemove", handleMouseMove); - container.addEventListener("mouseup", handleMouseUp); - container.addEventListener("mouseleave", handleMouseLeave); - - return () => { - // 清理触摸事件 - container.removeEventListener("touchstart", handleTouchStart); - container.removeEventListener("touchend", handleTouchEnd); - container.removeEventListener("touchcancel", handleTouchCancel); - // 清理鼠标事件 - container.removeEventListener("mousedown", handleMouseDown); - container.removeEventListener("mousemove", handleMouseMove); - container.removeEventListener("mouseup", handleMouseUp); - container.removeEventListener("mouseleave", handleMouseLeave); - }; - }, [goToNextPage, goToPrevPage]); + }, []); return (
-
- {loading &&
PDF加载中...
} - {error &&
{error}
} - - {pdfData && ( - -
- -
-
- )} - - {/* {numPages > 0 && ( - <> - -
- - {pageNumber} / {numPages} - -
- - )} */} +
+
diff --git a/src/pages/UI9/UI9.tsx b/src/pages/UI9/UI9.tsx index b5be166..b36a6d7 100644 --- a/src/pages/UI9/UI9.tsx +++ b/src/pages/UI9/UI9.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect, useCallback } from "react"; import "./UI9.css"; import { useNavigate } from "react-router-dom"; import DecorLine from "../../components/DecorLine"; @@ -16,15 +16,27 @@ const UI9: React.FC = () => { navigate(-1); }; - const handleConfirm = () => { - // 是否套餐待定 - const isPackageUndecided = true; - if (isPackageUndecided) { - //navigate("/u4"); - } else { - //navigate("/u5"); + const handleConfirm = useCallback(() => { + localStorage.removeItem("selectedExamId"); + localStorage.removeItem("lastIdCardNo"); + navigate("/"); + }, [navigate]); + + const [countdown, setCountdown] = useState(10); + const [backTime, setBackTime] = useState("确认(10S)"); + + useEffect(() => { + if (countdown > 0) { + setBackTime(`确认(${countdown}S)`); + const timer = setTimeout(() => setCountdown((prev) => prev - 1), 1000); + return () => clearTimeout(timer); } - }; + if (countdown <= 0) { + navigate("/"); + return; + } + setBackTime("确认"); + }, [countdown, navigate]); return (
@@ -46,7 +58,7 @@ const UI9: React.FC = () => { )}
- +
); diff --git a/src/utils/pdfWorker.ts b/src/utils/pdfWorker.ts new file mode 100644 index 0000000..9c384b2 --- /dev/null +++ b/src/utils/pdfWorker.ts @@ -0,0 +1,27 @@ +import { pdfjs } from "react-pdf"; + +let workerInitialized = false; + +/** + * 初始化 PDF.js Worker,确保只初始化一次 + */ +export const initPdfWorker = () => { + if (workerInitialized) { + window.electronAPI?.log("info", "[PDF Worker] 已初始化,跳过重复设置"); + return; + } + + const getWorkerSrc = () => { + if (window.location.protocol === "file:") { + return new URL("./pdf.worker.min.js", window.location.href).href; + } + return "/pdf.worker.min.js"; + }; + + const workerSrc = getWorkerSrc(); + pdfjs.GlobalWorkerOptions.workerSrc = workerSrc; + workerInitialized = true; + + window.electronAPI?.log("info", `[PDF Worker] 初始化完成,路径: ${workerSrc}`); +}; +