使用接口

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

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { pdfjs } from "react-pdf";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { Document, Page } from "react-pdf";
import "./UI7.css";
import "../../assets/css/basic.css";
import { useNavigate } from "react-router-dom";
@@ -8,115 +8,155 @@ import ConfirmButton from "../../components/ConfirmButton";
import DecorLine from "../../components/DecorLine";
import WaitButton from "../../components/WaitButton";
// @ts-ignore - Vite 会处理 ?raw 导入
import testPdfBase64Raw from "../../assets/testPdfBase64?raw";
const testPdfBase64 = testPdfBase64Raw.trim();
import { getTongyishuPdf } from "../../api/hisApi";
// 配置 PDF.js worker
const workerPath = new URL("pdf.worker.min.js", document.baseURI).href;
pdfjs.GlobalWorkerOptions.workerSrc = workerPath;
if (window.electronAPI) {
window.electronAPI.log("info", `[UI7] PDF Worker 路径: ${workerPath}`);
}
// 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染
const PdfRenderer = React.memo<{
pdfFiles: Uint8Array[];
loading: boolean;
error: string;
pageCounts: Record<number, number>;
onPageCountUpdate: (index: number, numPages: number) => void;
loadMessage: string;
}>(({ pdfFiles, loading, error, pageCounts, onPageCountUpdate, loadMessage }) => {
return (
<>
{loading && (
<div style={{ padding: "20px", textAlign: "center" , fontSize: "32px" }}>
<span style={{ fontSize: "32px" }}>{loadMessage}</span>
</div>
)}
{error && (
<div style={{ padding: "20px", color: "red", textAlign: "center" }}>
{error}
</div>
)}
{!loading &&
!error &&
pdfFiles.map((fileData, index) => (
<div key={index} style={{ marginBottom: "20px" }}>
<Document
file={{ data: fileData }}
loading=""
onLoadSuccess={({ numPages }) => onPageCountUpdate(index, numPages)}
>
{Array.from(
{ length: pageCounts[index] || 0 },
(_, pageIndex) => (
<Page
key={`pdf-${index}-page-${pageIndex + 1}`}
pageNumber={pageIndex + 1}
renderTextLayer={false}
renderAnnotationLayer={false}
width={900}
/>
)
)}
</Document>
</div>
))}
</>
);
});
PdfRenderer.displayName = "PdfRenderer";
const UI7: React.FC = () => {
const navigate = useNavigate();
const hasFetchedRef = useRef(false);
const [countdown, setCountdown] = useState(5);
const [showWaitButton, setShowWaitButton] = useState(true);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const dprRef = useRef<number>(1);
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
// PDF 文本提取相关状态
const [pdfText, setPdfText] = useState<string>("");
const [pdfFiles, setPdfFiles] = useState<Uint8Array[]>([]);
const [pageCounts, setPageCounts] =
useState<Record<number, number>>({});
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [loadMessage, setLoadMessage] = useState("PDF加载中...");
// 从 PDF 中提取文本
useEffect(() => {
const extractTextFromPdf = async () => {
try {
setLoading(true);
window.electronAPI?.log("info", "[UI7] 开始从 PDF 提取文本");
// 清理 base64 字符串(移除可能的换行和空格)
const cleanBase64 = testPdfBase64.replace(/\s/g, "");
window.electronAPI?.log("info", `[UI7] Base64 长度: ${cleanBase64.length}`);
// 将 base64 转换为 Uint8Array
const binaryString = atob(cleanBase64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
window.electronAPI?.log("info", `[UI7] 转换为 Uint8Array长度: ${bytes.length}`);
// 加载 PDF 文档(使用 Uint8Array
const loadingTask = pdfjs.getDocument({ data: bytes });
const pdf = await loadingTask.promise;
window.electronAPI?.log("info", `[UI7] PDF 加载成功,共 ${pdf.numPages}`);
// 提取所有页面的文本
let allText = "";
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const textContent = await page.getTextContent();
// 将文本项合并为字符串,保留换行信息
let pageText = "";
let lastY = null;
for (let i = 0; i < textContent.items.length; i++) {
const item = textContent.items[i] as any;
const currentY = item.transform ? item.transform[5] : null; // Y 坐标
// 如果 Y 坐标变化较大(超过行高的一半),认为是新行
if (lastY !== null && currentY !== null && Math.abs(currentY - lastY) > 5) {
pageText += "\n";
}
pageText += item.str;
// 如果当前项后面有空格标记,添加空格
if (item.hasEOL) {
pageText += "\n";
} else if (i < textContent.items.length - 1) {
const nextItem = textContent.items[i + 1] as any;
const nextX = nextItem.transform ? nextItem.transform[4] : null;
const currentX = item.transform ? item.transform[4] : null;
const currentWidth = item.width || 0;
// 如果下一个文本项距离较远,添加空格
if (currentX !== null && nextX !== null && (nextX - (currentX + currentWidth)) > 2) {
pageText += " ";
}
}
lastY = currentY;
// 辅助函数:从 URL 获取 PDF 的 Uint8Array
const fetchPdfBytes = async (url: string) => {
try {
if (window.electronAPI?.fetchPdf) {
window.electronAPI.log("info", `[UI7] 通过 Electron 获取 PDF: ${url}`);
const result = await window.electronAPI.fetchPdf(url);
if (result.success && result.data) {
const cleanBase64 = result.data.replace(/\s/g, "");
const binaryString = atob(cleanBase64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
allText += pageText + "\n\n";
return bytes;
}
throw new Error(result.error || "fetchPdf 返回失败");
}
window.electronAPI?.log("info", `[UI7] 通过 fetch 获取 PDF: ${url}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`网络请求失败: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
throw new Error(`获取 PDF 失败: ${(error as Error).message}`);
}
};
// 从多份 PDF 中获取数据
useEffect(() => {
if (hasFetchedRef.current) {
window.electronAPI?.log("info", "[UI7] 已获取过 PDF跳过重复请求");
return;
}
hasFetchedRef.current = true;
const fetchPdfs = async () => {
try {
const res = await getTongyishuPdf(
Number(localStorage.getItem("selectedExamId"))
);
if (res.Status !== 200) {
alert(`获取PDF数据失败: ${res.Message}`);
setLoadMessage(res.Message || "获取PDF数据失败");
return;
}
const pdfUrls: string[] = res.Data?.list_pdf_url || [];
if (!pdfUrls.length) {
throw new Error("未获取到任何 PDF 链接");
}
setLoading(true);
setError("");
setPdfFiles([]);
window.electronAPI?.log("info", "[UI7] 开始获取 PDF 文件");
const files: Uint8Array[] = [];
for (let idx = 0; idx < pdfUrls.length; idx++) {
const url = pdfUrls[idx];
window.electronAPI?.log(
"info",
`[UI7] 下载第 ${idx + 1} 份 PDF: ${url}`
);
const pdfBytes = await fetchPdfBytes(url);
files.push(pdfBytes);
}
window.electronAPI?.log("info", `[UI7] PDF 文本提取完成,长度: ${allText.length}`);
setPdfText(allText.trim());
setPdfFiles(files);
setLoading(false);
setError("");
} catch (err) {
const errorMsg = `PDF文本提取失败: ${err}`;
const errorMsg = `PDF取失败: ${err}`;
console.error(errorMsg);
window.electronAPI?.log("error", `[UI7] ${errorMsg}`);
setError("PDF 文本提取失败,请检查文件");
setError("PDF 取失败,请检查文件");
setLoading(false);
}
};
extractTextFromPdf();
fetchPdfs();
}, []);
useEffect(() => {
@@ -130,160 +170,21 @@ const UI7: React.FC = () => {
}
}, [countdown]);
useEffect(() => {
const initCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d", {
willReadFrequently: false,
alpha: true
});
if (!ctx) return;
// 获取 canvas 的显示尺寸
const rect = canvas.getBoundingClientRect();
// 使用更高的缩放倍数3倍来提高分辨率
const scale = 3;
dprRef.current = scale;
// 设置 canvas 内部尺寸高分辨率3倍
canvas.width = rect.width * scale;
canvas.height = rect.height * scale;
// 缩放上下文以匹配显示尺寸
ctx.scale(scale, scale);
// 设置 canvas 的 CSS 尺寸为显示尺寸
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
// 设置绘制样式(启用抗锯齿,优化参数)
ctx.strokeStyle = "#000000";
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.globalCompositeOperation = "source-over";
};
// 延迟初始化以确保 DOM 完全渲染
const timer = setTimeout(initCanvas, 100);
return () => clearTimeout(timer);
}, []);
const getCoordinates = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return { x: 0, y: 0 };
const rect = canvas.getBoundingClientRect();
if ("touches" in e) {
return {
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top,
};
} else {
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
}
};
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault();
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const { x, y } = getCoordinates(e);
// 记录起始点
lastPointRef.current = { x, y };
ctx.beginPath();
ctx.moveTo(x, y);
setIsDrawing(true);
};
const draw = (e: React.MouseEvent<HTMLCanvasElement> | React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault();
if (!isDrawing || !lastPointRef.current) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const { x, y } = getCoordinates(e);
const lastPoint = lastPointRef.current;
// 使用二次贝塞尔曲线平滑绘制
const midX = (lastPoint.x + x) / 2;
const midY = (lastPoint.y + y) / 2;
ctx.quadraticCurveTo(lastPoint.x, lastPoint.y, midX, midY);
ctx.stroke();
// 更新最后一个点
lastPointRef.current = { x, y };
};
const stopDrawing = () => {
setIsDrawing(false);
lastPointRef.current = null;
};
const clearCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// 清除整个 canvas使用显示坐标系统
const rect = canvas.getBoundingClientRect();
ctx.clearRect(0, 0, rect.width, rect.height);
// 重新初始化绘制样式
ctx.strokeStyle = "#000000";
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
};
const handleBack = () => {
const handleBack = useCallback(() => {
navigate(-1);
};
const handleConfirm = () => {
const canvas = canvasRef.current;
if (!canvas) {
// navigate("/UI7");
alert("签名不能为空,请签名后再提交");
return;
}
// 下载签名图片
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `签名_${new Date().getTime()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, "image/png");
}, [navigate]);
const handleConfirm = useCallback(() => {
navigate("/UI8");
};
}, [navigate]);
// 稳定 PDF 页数更新回调
const handlePageCountUpdate = useCallback((index: number, numPages: number) => {
setPageCounts((prev) => {
if (prev[index] === numPages) return prev;
return { ...prev, [index]: numPages };
});
}, []);
return (
<div className="basic-root">
@@ -294,46 +195,23 @@ const UI7: React.FC = () => {
<div className="ui7-text-wrapper">
<div className="ui7-text-content">
{loading && <div style={{ padding: "20px", textAlign: "center" }}>PDF文本提取中...</div>}
{error && <div style={{ padding: "20px", color: "red", textAlign: "center" }}>{error}</div>}
{!loading && !error && pdfText && (
<span className="paragraph_1">
{pdfText.split("\n").map((line, index) => (
<React.Fragment key={index}>
{line}
{index < pdfText.split("\n").length - 1 && <br />}
</React.Fragment>
))}
</span>
)}
<PdfRenderer
pdfFiles={pdfFiles}
loading={loading}
error={error}
pageCounts={pageCounts}
onPageCountUpdate={handlePageCountUpdate}
loadMessage={loadMessage}
/>
</div>
</div>
<span className="ui7-text_4"></span>
<div className="ui7-signature-wrapper">
<canvas
ref={canvasRef}
className="ui7-signature-canvas"
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={stopDrawing}
/>
<button className="ui7-clear-button" onClick={clearCanvas}>
</button>
</div>
<div className="basic-confirm-section">
<BackButton text="返回" onClick={handleBack} />
{showWaitButton ? (
<WaitButton text={`等待(${countdown})S`} />
) : (
<ConfirmButton text="提交" onClick={handleConfirm} />
<ConfirmButton text="确定" onClick={handleConfirm} />
)}
</div>
</div>