使用接口
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 向上滑动动画(下一页) */
|
/* 向上滑动动画(下一页) */
|
||||||
|
|||||||
@@ -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] 开始获取导检单 PDF,exam_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">
|
||||||
|
|||||||
@@ -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
27
src/utils/pdfWorker.ts
Normal 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}`);
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user