使用接口

This commit is contained in:
xianyi
2025-11-25 18:16:45 +08:00
parent 2571d52940
commit f3bea31e91
11 changed files with 439 additions and 582 deletions

View File

@@ -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<ApiResponse<PackagItemDetailResponse>> {
const response = await axiosInstance.post<
ApiResponse<PackagItemDetailResponse>
>("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<ApiResponse<PdfResponse>> {
const response = await axiosInstance.get<ApiResponse<PdfResponse>>(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<ApiResponse<PdfResponse>> {
const response = await axiosInstance.post<ApiResponse<PdfResponse>>(
): Promise<ApiResponse<DaojiandanPdfResponse>> {
const response = await axiosInstance.post<ApiResponse<DaojiandanPdfResponse>>(
"daojiandan-get",
{
exam_id,

View File

@@ -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(
<React.StrictMode>

View File

@@ -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}`);
}

View File

@@ -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 (
<div className="u4-root">
@@ -54,7 +57,7 @@ const U4: React.FC = () => {
<div
key={t.id}
className="u4-card"
onClick={() => setSelectedId(t.id)}
onClick={() => {setSelectedId(t.id); localStorage.setItem("selectedExamId", t.exam_id.toString())}}
>
<div className="u4-card-header">
<img

View File

@@ -1,12 +1,17 @@
import React from "react";
import React, { useEffect, useState } from "react";
import "./UI6.css";
import "../../assets/css/basic.css";
import { useNavigate } from "react-router-dom";
import BackButton from "../../components/BackButton";
import ConfirmButton from "../../components/ConfirmButton";
import DecorLine from "../../components/DecorLine";
import { getPackagItemDetail } from "../../api/hisApi";
const UI6: React.FC = () => {
// !
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<any[]>([]);
const [PackageInfo, setPackageInfo] = useState<any>({});
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 (
<div className="basic-root">
@@ -68,10 +72,10 @@ const UI6: React.FC = () => {
<DecorLine />
<span className="basic-paragraph">
{localStorage.getItem("name")}{localStorage.getItem("gender") === "男" ? "先生" : "女士"}
<br />
<br />
2022-02-219:00-9:30的体
{PackageInfo.appointment_datetime}
</span>
<div className="ui6-table-container">
@@ -83,11 +87,11 @@ const UI6: React.FC = () => {
</tr>
</thead>
<tbody>
{testData.map((item, index) => (
{ListData.map((item, index) => (
<tr key={index} className="ui6-table-row">
<td className="ui6-table-dept-cell">{item.department}</td>
<td className="ui6-table-dept-cell">{item.department_name}</td>
<td className="ui6-table-project-cell">
{item.project.map((project, pIndex) => (
{item.project_names.map((project: string, pIndex: number) => (
<div key={pIndex} className="ui6-project-item">
<span style={{ paddingLeft: 20 }}>{project}</span>
</div>
@@ -101,7 +105,7 @@ const UI6: React.FC = () => {
<div className="basic-confirm-section">
<BackButton text="返回" onClick={handleBack} />
<ConfirmButton text="签名" onClick={handleConfirm} />
<ConfirmButton text="确定" onClick={handleConfirm} />
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
.ui7-text-wrapper {
height: 623px;
height: 1080px;
width: 975px;
border: 2px solid #000;
border-radius: 30px;

View File

@@ -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<number, number>;
onPageCountUpdate: (index: number, numPages: number) => void;
loadMessage: string;
}>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, loadMessage }) => {
return (
<>
{loading && (
<div style={{ padding: "20px", textAlign: "center" , fontSize: "32px" }}>
<span style={{ fontSize: "32px" }}>{loadMessage}</span>
</div>
)}
{error && (
<div style={{ padding: "20px", color: "red", textAlign: "center" }}>
{error}
</div>
)}
{!loading &&
!error &&
pdfFiles.map((fileData, index) => (
<div key={index} style={{ marginBottom: "20px" }}>
<Document
file={{ data: fileData }}
loading=""
onLoadSuccess={({ numPages }) => onPageCountUpdate(index, numPages)}
>
{Array.from(
{ length: pageCounts[index] || 0 },
(_, pageIndex) => (
<Page
key={`pdf-${index}-page-${pageIndex + 1}`}
pageNumber={pageIndex + 1}
renderTextLayer={false}
renderAnnotationLayer={false}
width={900}
/>
)
)}
</Document>
</div>
))}
</>
);
});
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<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const dprRef = useRef<number>(1);
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
// PDF 文本提取相关状态
const [pdfText, setPdfText] = useState<string>("");
const [pdfFiles, setPdfFiles] = useState<Uint8Array[]>([]);
const [pageCounts, setPageCounts] =
useState<Record<number, number>>({});
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
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<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
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<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
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<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
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 (
<div className="basic-root">
@@ -294,46 +195,23 @@ const UI7: React.FC = () => {
<div className="ui7-text-wrapper">
<div className="ui7-text-content">
{loading && <div style={{ padding: "20px", textAlign: "center" }}>PDF文本提取中...</div>}
{error && <div style={{ padding: "20px", color: "red", textAlign: "center" }}>{error}</div>}
{!loading && !error && pdfText && (
<span className="paragraph_1">
{pdfText.split("\n").map((line, index) => (
<React.Fragment key={index}>
{line}
{index < pdfText.split("\n").length - 1 && <br />}
</React.Fragment>
))}
</span>
)}
<PdfRenderer
pdfFiles={pdfFiles}
loading={loading}
error={error}
pageCounts={pageCounts}
onPageCountUpdate={handlePageCountUpdate}
loadMessage={loadMessage}
/>
</div>
</div>
<span className="ui7-text_4"></span>
<div className="ui7-signature-wrapper">
<canvas
ref={canvasRef}
className="ui7-signature-canvas"
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={stopDrawing}
/>
<button className="ui7-clear-button" onClick={clearCanvas}>
</button>
</div>
<div className="basic-confirm-section">
<BackButton text="返回" onClick={handleBack} />
{showWaitButton ? (
<WaitButton text={`等待(${countdown})S`} />
) : (
<ConfirmButton text="提交" onClick={handleConfirm} />
<ConfirmButton text="确定" onClick={handleConfirm} />
)}
</div>
</div>

View File

@@ -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;
}
/* 向上滑动动画(下一页) */

View File

@@ -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<number, number>;
onPageCountUpdate: (index: number, numPages: number) => void;
onDocumentLoadError: (error: Error) => void;
}>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, onDocumentLoadError }) => {
return (
<>
{loading && <div className="ui8-loading">PDF加载中...</div>}
{error && <div className="ui8-error">{error}</div>}
if (window.electronAPI) {
window.electronAPI.log("info", `PDF Worker 路径: ${workerPath}`);
window.electronAPI.log("info", `Base URI: ${document.baseURI}`);
}
{!loading &&
!error &&
pdfFiles.map((fileData, index) => (
<div key={index} style={{ marginBottom: "20px", width: "100%" }}>
<Document
file={fileData}
loading=""
onLoadSuccess={({ numPages }) => onPageCountUpdate(index, numPages)}
onLoadError={onDocumentLoadError}
>
{Array.from({ length: pageCounts[index] || 0 }, (_, pageIndex) => (
<div key={`pdf-${index}-page-${pageIndex + 1}`} className="ui8-pdf-page-wrapper">
<Page
pageNumber={pageIndex + 1}
renderTextLayer={false}
renderAnnotationLayer={false}
className="ui8-pdf-page"
width={920}
/>
</div>
))}
</Document>
</div>
))}
</>
);
});
PdfRenderer.displayName = "PdfRenderer";
// 使用本地 PDF 文件进行测试
const PDF_URL = testPdf;
const UI8: React.FC = () => {
const navigate = useNavigate();
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [isPrinting, setIsPrinting] = useState<boolean>(false);
const [pdfData, setPdfData] = useState<string | null>(null);
const [isAnimating, setIsAnimating] = useState<boolean>(false);
const [animationDirection, setAnimationDirection] = useState<"up" | "down" | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
const [pdfFiles, setPdfFiles] = useState<string[]>([]);
const [pageCounts, setPageCounts] = useState<Record<number, number>>({});
const [originPdfUrls, setOriginPdfUrls] = useState<string[]>([]);
const getExamId = () => {
const storedId = localStorage.getItem("selectedExamId");
return storedId || "";
};
const arrayBufferToDataUrl = (arrayBuffer: ArrayBuffer) => {
return new Promise<string>((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] 开始获取导检单 PDFexam_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 (
<div className="basic-root">
<div className="basic-white-block">
<div className="basic-content">
<div className="ui8-pdf-container" ref={scrollContainerRef}>
{loading && <div className="ui8-loading">PDF加载中...</div>}
{error && <div className="ui8-error">{error}</div>}
{pdfData && (
<Document
file={pdfData}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
loading=""
>
<div
className={`ui8-pdf-page-wrapper ${
animationDirection === "up" ? "slide-up" :
animationDirection === "down" ? "slide-down" : ""
}`}
>
<Page
pageNumber={pageNumber}
renderTextLayer={false}
renderAnnotationLayer={false}
className="ui8-pdf-page"
width={920}
/>
</div>
</Document>
)}
{/* {numPages > 0 && (
<>
<div className="ui8-pdf-controls">
<span className="ui8-page-info">
{pageNumber} / {numPages}
</span>
</div>
</>
)} */}
<div className="ui8-pdf-container">
<PdfRenderer
pdfFiles={pdfFiles}
loading={loading}
error={error}
pageCounts={pageCounts}
onPageCountUpdate={handlePageCountUpdate}
onDocumentLoadError={onDocumentLoadError}
/>
</div>
<div className="ui8-right-section">

View File

@@ -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 (
<div className="ui9-root">
@@ -46,7 +58,7 @@ const UI9: React.FC = () => {
)}
<div className="ui9-confirm-section">
<BackButton text="返回" onClick={handleBack} />
<ConfirmButton text="确认" onClick={handleConfirm} />
<ConfirmButton text={backTime} onClick={handleConfirm} />
</div>
</div>
);

27
src/utils/pdfWorker.ts Normal file
View File

@@ -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}`);
};