使用接口

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响应 // PDF响应
export interface PdfResponse { export interface PdfResponse {
list_pdf_url: string[];
message: string | null;
}
export interface DaojiandanPdfResponse {
exam_id: number;
pdf_url: string; pdf_url: string;
message: string | null; message: string | null;
} }
@@ -196,14 +202,14 @@ export async function getPackagItemDetail(
): Promise<ApiResponse<PackagItemDetailResponse>> { ): Promise<ApiResponse<PackagItemDetailResponse>> {
const response = await axiosInstance.post< const response = await axiosInstance.post<
ApiResponse<PackagItemDetailResponse> ApiResponse<PackagItemDetailResponse>
>("optional-item-list", { >("package-item-list", {
id_no, id_no,
}); });
return response.data; return response.data;
} }
/** /**
* 5. 获取体检知情同意书PDF * 5. 获取体检知情同意书PDFurlList
*/ */
export async function getTongyishuPdf( export async function getTongyishuPdf(
exam_id: number exam_id: number
@@ -217,6 +223,15 @@ export async function getTongyishuPdf(
return response.data; 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 * 6. 体检知情同意书签名返回生成的知情同意书PDF
* @param exam_id 体检ID * @param exam_id 体检ID
@@ -247,8 +262,8 @@ export async function submitTongyishuSign(
*/ */
export async function getDaojiandanPdf( export async function getDaojiandanPdf(
exam_id: number exam_id: number
): Promise<ApiResponse<PdfResponse>> { ): Promise<ApiResponse<DaojiandanPdfResponse>> {
const response = await axiosInstance.post<ApiResponse<PdfResponse>>( const response = await axiosInstance.post<ApiResponse<DaojiandanPdfResponse>>(
"daojiandan-get", "daojiandan-get",
{ {
exam_id, exam_id,

View File

@@ -66,6 +66,10 @@ import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom"; import { HashRouter } from "react-router-dom";
import App from "./App"; import App from "./App";
import { initPdfWorker } from "./utils/pdfWorker";
// 在应用启动时初始化 PDF Worker确保只初始化一次
initPdfWorker();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode> <React.StrictMode>

View File

@@ -28,6 +28,8 @@ const U2: React.FC = () => {
.then((res) => { .then((res) => {
if (res.Status === 200) { if (res.Status === 200) {
setPatientInfo(res.Data); setPatientInfo(res.Data);
localStorage.setItem("name", res.Data.name);
localStorage.setItem("gender", res.Data.gender_name);
} else { } else {
alert(`获取用户信息失败: ${res.Message}`); alert(`获取用户信息失败: ${res.Message}`);
} }

View File

@@ -11,6 +11,7 @@ interface testType {
title: string; title: string;
desc: string; desc: string;
taboo: string; taboo: string;
exam_id: number;
} }
const U4: React.FC = () => { const U4: React.FC = () => {
@@ -32,6 +33,7 @@ const U4: React.FC = () => {
title: it.combination_name, title: it.combination_name,
desc: "", desc: "",
taboo: "", taboo: "",
exam_id: it.physical_exam_id,
})); }));
setTest(items); setTest(items);
} else { } else {
@@ -43,6 +45,7 @@ const U4: React.FC = () => {
React.useEffect(() => { React.useEffect(() => {
console.log("选择的项目", selectedId); console.log("选择的项目", selectedId);
}, [selectedId]); }, [selectedId]);
return ( return (
<div className="u4-root"> <div className="u4-root">
@@ -54,7 +57,7 @@ const U4: React.FC = () => {
<div <div
key={t.id} key={t.id}
className="u4-card" className="u4-card"
onClick={() => setSelectedId(t.id)} onClick={() => {setSelectedId(t.id); localStorage.setItem("selectedExamId", t.exam_id.toString())}}
> >
<div className="u4-card-header"> <div className="u4-card-header">
<img <img

View File

@@ -1,12 +1,17 @@
import React from "react"; import React, { useEffect, useState } from "react";
import "./UI6.css"; import "./UI6.css";
import "../../assets/css/basic.css"; import "../../assets/css/basic.css";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import BackButton from "../../components/BackButton"; import BackButton from "../../components/BackButton";
import ConfirmButton from "../../components/ConfirmButton"; import ConfirmButton from "../../components/ConfirmButton";
import DecorLine from "../../components/DecorLine"; import DecorLine from "../../components/DecorLine";
import { getPackagItemDetail } from "../../api/hisApi";
const UI6: React.FC = () => { const UI6: React.FC = () => {
// !
localStorage.setItem("selectedExamId", "100030906");
const navigate = useNavigate(); const navigate = useNavigate();
const handleBack = () => { const handleBack = () => {
@@ -17,48 +22,47 @@ const UI6: React.FC = () => {
navigate("/UI7"); navigate("/UI7");
}; };
const testData = [ const [ListData, setListData] = useState<any[]>([]);
{ const [PackageInfo, setPackageInfo] = useState<any>({});
"department": "B超科室", useEffect(() => {
"project": ["甲状腺B超", "腹部B超" , "乳腺B超"] getListData();
}, }, []);
{
"department": "血常规科室",
"project": ["血常规", "血型"] const getListData = async () => {
}, const id_no = localStorage.getItem("lastIdCardNo");
{ if (!id_no) {
"department": "心电图科室", alert("请先输入身份证号");
"project": ["心电图", "心电图"] return;
},
{
"department": "心电图科室",
"project": ["心电图", "心电图"]
},
{
"department": "心电图科室",
"project": ["心电图", "心电图"]
},
{
"department": "心电图科室",
"project": ["心电图", "心电图"]
},
{
"department": "心电图科室",
"project": ["心电图", "心电图"]
},
{
"department": "心电图科室",
"project": ["心电图", "心电图"]
},
{
"department": "心电图科室",
"project": ["心电图", "心电图"]
},
{
"department": "心电图科室",
"project": ["心电图", "心电图","心电图", "心电图","心电图", "心电图","心电图", "心电图","心电图", "心电图"]
} }
] 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 ( return (
<div className="basic-root"> <div className="basic-root">
@@ -68,10 +72,10 @@ const UI6: React.FC = () => {
<DecorLine /> <DecorLine />
<span className="basic-paragraph"> <span className="basic-paragraph">
{localStorage.getItem("name")}{localStorage.getItem("gender") === "男" ? "先生" : "女士"}
<br /> <br />
<br /> <br />
2022-02-219:00-9:30的体 {PackageInfo.appointment_datetime}
</span> </span>
<div className="ui6-table-container"> <div className="ui6-table-container">
@@ -83,11 +87,11 @@ const UI6: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{testData.map((item, index) => ( {ListData.map((item, index) => (
<tr key={index} className="ui6-table-row"> <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"> <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"> <div key={pIndex} className="ui6-project-item">
<span style={{ paddingLeft: 20 }}>{project}</span> <span style={{ paddingLeft: 20 }}>{project}</span>
</div> </div>
@@ -101,7 +105,7 @@ const UI6: React.FC = () => {
<div className="basic-confirm-section"> <div className="basic-confirm-section">
<BackButton text="返回" onClick={handleBack} /> <BackButton text="返回" onClick={handleBack} />
<ConfirmButton text="签名" onClick={handleConfirm} /> <ConfirmButton text="确定" onClick={handleConfirm} />
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { pdfjs } from "react-pdf"; import { Document, Page } from "react-pdf";
import "./UI7.css"; import "./UI7.css";
import "../../assets/css/basic.css"; import "../../assets/css/basic.css";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -8,115 +8,155 @@ import ConfirmButton from "../../components/ConfirmButton";
import DecorLine from "../../components/DecorLine"; import DecorLine from "../../components/DecorLine";
import WaitButton from "../../components/WaitButton"; import WaitButton from "../../components/WaitButton";
// @ts-ignore - Vite 会处理 ?raw 导入 import { getTongyishuPdf } from "../../api/hisApi";
import testPdfBase64Raw from "../../assets/testPdfBase64?raw";
const testPdfBase64 = testPdfBase64Raw.trim();
// 配置 PDF.js worker // 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染
const workerPath = new URL("pdf.worker.min.js", document.baseURI).href; const PdfRenderer = React.memo<{
pdfjs.GlobalWorkerOptions.workerSrc = workerPath; pdfFiles: Uint8Array[];
loading: boolean;
if (window.electronAPI) { error: string;
window.electronAPI.log("info", `[UI7] PDF Worker 路径: ${workerPath}`); 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 UI7: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const hasFetchedRef = useRef(false);
const [countdown, setCountdown] = useState(5); const [countdown, setCountdown] = useState(5);
const [showWaitButton, setShowWaitButton] = useState(true); const [showWaitButton, setShowWaitButton] = useState(true);
const canvasRef = useRef<HTMLCanvasElement>(null); const [pdfFiles, setPdfFiles] = useState<Uint8Array[]>([]);
const [isDrawing, setIsDrawing] = useState(false); const [pageCounts, setPageCounts] =
const dprRef = useRef<number>(1); useState<Record<number, number>>({});
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
// PDF 文本提取相关状态
const [pdfText, setPdfText] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>(""); 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 字符串(移除可能的换行和空格) // 辅助函数:从 URL 获取 PDF 的 Uint8Array
const cleanBase64 = testPdfBase64.replace(/\s/g, ""); const fetchPdfBytes = async (url: string) => {
window.electronAPI?.log("info", `[UI7] Base64 长度: ${cleanBase64.length}`); try {
if (window.electronAPI?.fetchPdf) {
// 将 base64 转换为 Uint8Array window.electronAPI.log("info", `[UI7] 通过 Electron 获取 PDF: ${url}`);
const binaryString = atob(cleanBase64); const result = await window.electronAPI.fetchPdf(url);
const bytes = new Uint8Array(binaryString.length); if (result.success && result.data) {
for (let i = 0; i < binaryString.length; i++) { const cleanBase64 = result.data.replace(/\s/g, "");
bytes[i] = binaryString.charCodeAt(i); const binaryString = atob(cleanBase64);
} const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
window.electronAPI?.log("info", `[UI7] 转换为 Uint8Array长度: ${bytes.length}`); bytes[i] = binaryString.charCodeAt(i);
// 加载 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;
} }
return bytes;
}
throw new Error(result.error || "fetchPdf 返回失败");
}
allText += pageText + "\n\n"; 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;
} }
window.electronAPI?.log("info", `[UI7] PDF 文本提取完成,长度: ${allText.length}`); const pdfUrls: string[] = res.Data?.list_pdf_url || [];
setPdfText(allText.trim()); 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);
}
setPdfFiles(files);
setLoading(false); setLoading(false);
setError(""); setError("");
} catch (err) { } catch (err) {
const errorMsg = `PDF文本提取失败: ${err}`; const errorMsg = `PDF取失败: ${err}`;
console.error(errorMsg); console.error(errorMsg);
window.electronAPI?.log("error", `[UI7] ${errorMsg}`); window.electronAPI?.log("error", `[UI7] ${errorMsg}`);
setError("PDF 文本提取失败,请检查文件"); setError("PDF 取失败,请检查文件");
setLoading(false); setLoading(false);
} }
}; };
extractTextFromPdf(); fetchPdfs();
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -130,160 +170,21 @@ const UI7: React.FC = () => {
} }
}, [countdown]); }, [countdown]);
useEffect(() => { const handleBack = useCallback(() => {
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 = () => {
navigate(-1); navigate(-1);
}; }, [navigate]);
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");
const handleConfirm = useCallback(() => {
navigate("/UI8"); navigate("/UI8");
}; }, [navigate]);
// 稳定 PDF 页数更新回调
const handlePageCountUpdate = useCallback((index: number, numPages: number) => {
setPageCounts((prev) => {
if (prev[index] === numPages) return prev;
return { ...prev, [index]: numPages };
});
}, []);
return ( return (
<div className="basic-root"> <div className="basic-root">
@@ -294,46 +195,23 @@ const UI7: React.FC = () => {
<div className="ui7-text-wrapper"> <div className="ui7-text-wrapper">
<div className="ui7-text-content"> <div className="ui7-text-content">
{loading && <div style={{ padding: "20px", textAlign: "center" }}>PDF文本提取中...</div>} <PdfRenderer
{error && <div style={{ padding: "20px", color: "red", textAlign: "center" }}>{error}</div>} pdfFiles={pdfFiles}
{!loading && !error && pdfText && ( loading={loading}
<span className="paragraph_1"> error={error}
{pdfText.split("\n").map((line, index) => ( pageCounts={pageCounts}
<React.Fragment key={index}> onPageCountUpdate={handlePageCountUpdate}
{line} loadMessage={loadMessage}
{index < pdfText.split("\n").length - 1 && <br />} />
</React.Fragment>
))}
</span>
)}
</div> </div>
</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"> <div className="basic-confirm-section">
<BackButton text="返回" onClick={handleBack} /> <BackButton text="返回" onClick={handleBack} />
{showWaitButton ? ( {showWaitButton ? (
<WaitButton text={`等待(${countdown})S`} /> <WaitButton text={`等待(${countdown})S`} />
) : ( ) : (
<ConfirmButton text="提交" onClick={handleConfirm} /> <ConfirmButton text="确定" onClick={handleConfirm} />
)} )}
</div> </div>
</div> </div>

View File

@@ -77,20 +77,27 @@
border-bottom: none; border-bottom: none;
} }
/* PDF 展示容器 */ /* PDF 展示容器 - 改为滚动显示模式 */
.ui8-pdf-container { .ui8-pdf-container {
height: 1000px; height: 1200px;
overflow-y: auto;
overflow-x: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
user-select: none; /* 隐藏滚动条但保持滚动功能 */
touch-action: pan-y; /* 允许垂直触摸滑动 */ scrollbar-width: none; /* Firefox */
-webkit-user-select: none; -ms-overflow-style: none; /* IE 和 Edge */
-webkit-touch-callout: none; padding: 20px 10px;
} }
.ui8-pdf-container::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* PDF 页面包装器 - 不再需要,保留以防兼容性 */
.ui8-pdf-page-wrapper { .ui8-pdf-page-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -100,8 +107,6 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
max-width: 100%; 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 React, { useState, useEffect, useCallback } from "react";
import { Document, Page, pdfjs } from "react-pdf"; import { Document, Page } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css"; import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "./UI8.css"; import "./UI8.css";
import "../../assets/css/basic.css"; import "../../assets/css/basic.css";
@@ -8,72 +8,137 @@ import BackButton from "../../components/BackButton";
import ConfirmButton from "../../components/ConfirmButton"; import ConfirmButton from "../../components/ConfirmButton";
import ui8A from "../../assets/ui8A.png"; import ui8A from "../../assets/ui8A.png";
import ui8B from "../../assets/ui8B.png"; import ui8B from "../../assets/ui8B.png";
import testPdf from "../../assets/testPdf.pdf"; import { getDaojiandanPdf } from "../../api/hisApi";
// 配置 PDF.js worker // 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染
// 开发环境Vite 会从根路径提供 public/pdf.worker.min.js const PdfRenderer = React.memo<{
// 生产环境Electron 需要相对于 index.html 的路径 pdfFiles: string[];
const workerPath = new URL("pdf.worker.min.js", document.baseURI).href; loading: boolean;
pdfjs.GlobalWorkerOptions.workerSrc = workerPath; 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) { {!loading &&
window.electronAPI.log("info", `PDF Worker 路径: ${workerPath}`); !error &&
window.electronAPI.log("info", `Base URI: ${document.baseURI}`); 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 UI8: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [isPrinting, setIsPrinting] = useState<boolean>(false); const [isPrinting, setIsPrinting] = useState<boolean>(false);
const [pdfData, setPdfData] = useState<string | null>(null); const [pdfFiles, setPdfFiles] = useState<string[]>([]);
const [isAnimating, setIsAnimating] = useState<boolean>(false); const [pageCounts, setPageCounts] = useState<Record<number, number>>({});
const [animationDirection, setAnimationDirection] = useState<"up" | "down" | null>(null); const [originPdfUrls, setOriginPdfUrls] = useState<string[]>([]);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const touchStartRef = useRef<{ x: number; y: number } | null>(null); 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数据 // 加载PDF数据
useEffect(() => { useEffect(() => {
const loadPdf = async () => { const loadPdf = async () => {
try { try {
setLoading(true); setLoading(true);
window.electronAPI?.log("info", `开始加载PDF: ${PDF_URL}`); setError("");
setPdfFiles([]);
setPageCounts({});
// 本地文件直接使用,无需通过 fetchPdf const examId = getExamId();
if (PDF_URL.startsWith("/") || PDF_URL.startsWith("blob:") || PDF_URL.includes("assets")) { window.electronAPI?.log("info", `[UI8] 开始获取导检单 PDFexam_id=${examId}`);
window.electronAPI?.log("info", `检测到本地PDF文件直接加载: ${PDF_URL}`); const res = await getDaojiandanPdf(parseInt(examId, 10));
setPdfData(PDF_URL); if (res.Status !== 200) {
setLoading(false); throw new Error(res.Message || "获取导检单PDF失败");
return;
} }
// 远程 URL 需要通过 Electron 绕过 CORS const pdfUrl = res.Data?.pdf_url;
if (window.electronAPI?.fetchPdf) { if (!pdfUrl) {
window.electronAPI.log("info", `通过Electron下载远程PDF: ${PDF_URL}`); throw new Error("未获取到导检单 PDF");
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);
} }
setOriginPdfUrls([pdfUrl]);
window.electronAPI?.log("info", `[UI8] 下载导检单 PDF: ${pdfUrl}`);
const dataUrl = await fetchPdfDataUrl(pdfUrl);
setPdfFiles([dataUrl]);
} catch (err) { } catch (err) {
const errorMsg = `PDF fetch error: ${err}`; const errorMsg = `导检单PDF获取失败: ${(err as Error).message || err}`;
console.error(errorMsg); console.error(errorMsg);
window.electronAPI?.log("error", errorMsg); window.electronAPI?.log("error", `[UI8] ${errorMsg}`);
setError("PDF加载失败,请检查网络连接"); setError("PDF 获取失败,请稍后重试");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -82,12 +147,12 @@ const UI8: React.FC = () => {
loadPdf(); loadPdf();
}, []); }, []);
const handleBack = () => { const handleBack = useCallback(() => {
navigate(-1); navigate(-1);
}; }, [navigate]);
// 打印PDF功能 // 打印PDF功能
const handleConfirm = async () => { const handleConfirm = useCallback(async () => {
if (!window.electronAPI?.printPdf) { if (!window.electronAPI?.printPdf) {
const errorMsg = "打印功能不可用,请在 Electron 环境中运行"; const errorMsg = "打印功能不可用,请在 Electron 环境中运行";
window.electronAPI?.log("error", errorMsg); window.electronAPI?.log("error", errorMsg);
@@ -95,7 +160,8 @@ const UI8: React.FC = () => {
return; return;
} }
if (!pdfData) { const primaryPdf = pdfFiles[0] || originPdfUrls[0];
if (!primaryPdf) {
const errorMsg = "PDF 尚未加载完成,请稍候"; const errorMsg = "PDF 尚未加载完成,请稍候";
window.electronAPI?.log("warn", errorMsg); window.electronAPI?.log("warn", errorMsg);
alert(errorMsg); alert(errorMsg);
@@ -104,9 +170,11 @@ const UI8: React.FC = () => {
setIsPrinting(true); setIsPrinting(true);
try { try {
// 本地文件直接传原始路径,远程文件传 base64 data URI const printData =
const printData = pdfData.startsWith("data:") ? pdfData : PDF_URL; primaryPdf.startsWith("data:") && pdfFiles[0]
const dataType = pdfData.startsWith("data:") ? "base64数据" : "本地文件路径"; ? primaryPdf
: originPdfUrls[0];
const dataType = printData.startsWith("data:") ? "base64数据" : "远程文件路径";
window.electronAPI.log("info", `开始打印PDF (${dataType}): ${printData.substring(0, 100)}...`); window.electronAPI.log("info", `开始打印PDF (${dataType}): ${printData.substring(0, 100)}...`);
const result = await window.electronAPI.printPdf(printData); const result = await window.electronAPI.printPdf(printData);
@@ -124,202 +192,41 @@ const UI8: React.FC = () => {
alert("打印失败,请重试"); alert("打印失败,请重试");
} finally { } finally {
setIsPrinting(false); setIsPrinting(false);
navigate("/UI9");
} }
}; }, [originPdfUrls, pdfFiles]);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { const handlePageCountUpdate = useCallback((index: number, numPages: number) => {
window.electronAPI?.log("info", `PDF渲染成功${numPages}`); window.electronAPI?.log("info", `[UI8] PDF渲染成功 (index=${index}),共 ${numPages}`);
setNumPages(numPages); setPageCounts((prev) => {
if (prev[index] === numPages) return prev;
return { ...prev, [index]: numPages };
});
setLoading(false); setLoading(false);
setError(""); setError("");
}; }, []);
const onDocumentLoadError = (error: Error) => { const onDocumentLoadError = useCallback((error: Error) => {
const errorMsg = `PDF渲染失败: ${error.message || error}`; const errorMsg = `PDF渲染失败: ${error.message || error}`;
console.error("PDF load error:", error); console.error("PDF load error:", error);
window.electronAPI?.log("error", errorMsg); window.electronAPI?.log("error", errorMsg);
window.electronAPI?.log("error", `PDF数据: ${pdfData?.substring(0, 100)}...`);
setError("PDF 加载失败,请检查网络连接"); setError("PDF 加载失败,请检查网络连接");
setLoading(false); 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 ( return (
<div className="basic-root"> <div className="basic-root">
<div className="basic-white-block"> <div className="basic-white-block">
<div className="basic-content"> <div className="basic-content">
<div className="ui8-pdf-container" ref={scrollContainerRef}> <div className="ui8-pdf-container">
{loading && <div className="ui8-loading">PDF加载中...</div>} <PdfRenderer
{error && <div className="ui8-error">{error}</div>} pdfFiles={pdfFiles}
loading={loading}
{pdfData && ( error={error}
<Document pageCounts={pageCounts}
file={pdfData} onPageCountUpdate={handlePageCountUpdate}
onLoadSuccess={onDocumentLoadSuccess} onDocumentLoadError={onDocumentLoadError}
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> </div>
<div className="ui8-right-section"> <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 "./UI9.css";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import DecorLine from "../../components/DecorLine"; import DecorLine from "../../components/DecorLine";
@@ -16,15 +16,27 @@ const UI9: React.FC = () => {
navigate(-1); navigate(-1);
}; };
const handleConfirm = () => { const handleConfirm = useCallback(() => {
// 是否套餐待定 localStorage.removeItem("selectedExamId");
const isPackageUndecided = true; localStorage.removeItem("lastIdCardNo");
if (isPackageUndecided) { navigate("/");
//navigate("/u4"); }, [navigate]);
} else {
//navigate("/u5"); 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 ( return (
<div className="ui9-root"> <div className="ui9-root">
@@ -46,7 +58,7 @@ const UI9: React.FC = () => {
)} )}
<div className="ui9-confirm-section"> <div className="ui9-confirm-section">
<BackButton text="返回" onClick={handleBack} /> <BackButton text="返回" onClick={handleBack} />
<ConfirmButton text="确认" onClick={handleConfirm} /> <ConfirmButton text={backTime} onClick={handleConfirm} />
</div> </div>
</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}`);
};