使用接口
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user