Merge branch 'main' of https://git.ambigrat.com/hom/yuanhe-checkin-electron
This commit is contained in:
19
src/electron.d.ts
vendored
19
src/electron.d.ts
vendored
@@ -1,9 +1,26 @@
|
||||
// Electron API 类型声明
|
||||
interface ElectronPrinterInfo {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
status?: number;
|
||||
isDefault?: boolean;
|
||||
options?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
fetchPdf: (
|
||||
pdfUrl: string
|
||||
) => Promise<{ success: boolean; data?: string; error?: string }>;
|
||||
printPdf: (pdfUrl: string) => Promise<{ success: boolean; error?: string }>;
|
||||
printPdf: (
|
||||
pdfUrl: string,
|
||||
options?: { printerName?: string }
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
getPrinters: () => Promise<{
|
||||
success: boolean;
|
||||
printers?: ElectronPrinterInfo[];
|
||||
error?: string;
|
||||
}>;
|
||||
startIdCardListen: () => Promise<any>;
|
||||
stopIdCardListen: () => Promise<any>;
|
||||
onIdCardData: (callback: (data: any) => void) => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.ui7-text-wrapper {
|
||||
height: 1080px;
|
||||
height: 670px;
|
||||
width: 975px;
|
||||
border: 2px solid #000;
|
||||
border-radius: 30px;
|
||||
@@ -7,12 +7,15 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
/* 隐藏滚动条但保持滚动功能 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 和 Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE 和 Edge */
|
||||
}
|
||||
|
||||
.ui7-text-wrapper::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
display: none;
|
||||
/* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
.ui7-text-content {
|
||||
@@ -97,4 +100,4 @@
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import ConfirmButton from "../../components/ConfirmButton";
|
||||
import DecorLine from "../../components/DecorLine";
|
||||
import WaitButton from "../../components/WaitButton";
|
||||
|
||||
import { getTongyishuPdf } from "../../api/hisApi";
|
||||
import { getTongyishuPdf, submitDaojiandanSign } from "../../api/hisApi";
|
||||
|
||||
// 独立的 PDF 渲染组件,使用 React.memo 避免不必要的重新渲染
|
||||
const PdfRenderer = React.memo<{
|
||||
@@ -22,7 +22,7 @@ const PdfRenderer = React.memo<{
|
||||
return (
|
||||
<>
|
||||
{loading && (
|
||||
<div style={{ padding: "20px", textAlign: "center" , fontSize: "32px" }}>
|
||||
<div style={{ padding: "20px", textAlign: "center", fontSize: "32px" }}>
|
||||
<span style={{ fontSize: "32px" }}>{loadMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -64,6 +64,9 @@ PdfRenderer.displayName = "PdfRenderer";
|
||||
const UI7: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const hasFetchedRef = useRef(false);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const dprRef = useRef<number>(1);
|
||||
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const [countdown, setCountdown] = useState(5);
|
||||
const [showWaitButton, setShowWaitButton] = useState(true);
|
||||
const [pdfFiles, setPdfFiles] = useState<Uint8Array[]>([]);
|
||||
@@ -72,6 +75,8 @@ const UI7: React.FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loadMessage, setLoadMessage] = useState("PDF加载中...");
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
|
||||
// 辅助函数:从 URL 获取 PDF 的 Uint8Array
|
||||
@@ -143,7 +148,7 @@ const UI7: React.FC = () => {
|
||||
const pdfBytes = await fetchPdfBytes(url);
|
||||
files.push(pdfBytes);
|
||||
}
|
||||
|
||||
|
||||
setPdfFiles(files);
|
||||
setLoading(false);
|
||||
setError("");
|
||||
@@ -170,13 +175,205 @@ const UI7: React.FC = () => {
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
useEffect(() => {
|
||||
const initCanvas = () => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d", {
|
||||
willReadFrequently: true,
|
||||
alpha: true,
|
||||
});
|
||||
if (!ctx) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scale = 3;
|
||||
dprRef.current = scale;
|
||||
canvas.width = rect.width * scale;
|
||||
canvas.height = rect.height * scale;
|
||||
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.lineWidth = 5;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
};
|
||||
|
||||
const timer = setTimeout(initCanvas, 150);
|
||||
window.addEventListener("resize", initCanvas);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
window.removeEventListener("resize", initCanvas);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getCoordinates = useCallback(
|
||||
(event: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return { x: 0, y: 0 };
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const startDrawing = useCallback(
|
||||
(event: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const { x, y } = getCoordinates(event);
|
||||
lastPointRef.current = { x, y };
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
setIsDrawing(true);
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
},
|
||||
[getCoordinates]
|
||||
);
|
||||
|
||||
const draw = useCallback(
|
||||
(event: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
if (event.cancelable) {
|
||||
event.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(event);
|
||||
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 };
|
||||
},
|
||||
[getCoordinates, isDrawing]
|
||||
);
|
||||
|
||||
const stopDrawing = useCallback(
|
||||
(event?: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
setIsDrawing(false);
|
||||
lastPointRef.current = null;
|
||||
if (event && canvasRef.current?.hasPointerCapture(event.pointerId)) {
|
||||
canvasRef.current.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const clearCanvas = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
}, []);
|
||||
|
||||
const isCanvasBlank = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return true;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return true;
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const pixelBuffer = new Uint32Array(imageData.data.buffer);
|
||||
return !pixelBuffer.some((color) => color !== 0);
|
||||
}, []);
|
||||
|
||||
const canvasToBlob = useCallback((canvas: HTMLCanvasElement) => {
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error("签名生成失败,请重试"));
|
||||
}
|
||||
},
|
||||
"image/png",
|
||||
1
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigate(-1);
|
||||
}, [navigate]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
navigate("/UI8");
|
||||
}, [navigate]);
|
||||
const execute = async () => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
alert("画布初始化失败,请刷新页面");
|
||||
return;
|
||||
}
|
||||
if (isCanvasBlank()) {
|
||||
alert("请先完成签名后再继续");
|
||||
return;
|
||||
}
|
||||
const examIdStr = localStorage.getItem("selectedExamId");
|
||||
const examId = Number(examIdStr);
|
||||
if (!examIdStr || Number.isNaN(examId)) {
|
||||
alert("未找到体检ID,请返回重试");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const blob = await canvasToBlob(canvas);
|
||||
|
||||
// 仍在本地缓存一下,方便 UI8 等页面复用
|
||||
try {
|
||||
const dataUrl = canvas.toDataURL("image/png");
|
||||
localStorage.setItem("consentSignature", dataUrl);
|
||||
} catch (cacheErr) {
|
||||
window.electronAPI?.log(
|
||||
"warn",
|
||||
`[UI7] 签名缓存失败: ${(cacheErr as Error).message}`
|
||||
);
|
||||
}
|
||||
|
||||
window.electronAPI?.log(
|
||||
"info",
|
||||
`[UI7] 提交导检单签名,exam_id=${examId}`
|
||||
);
|
||||
const res = await submitDaojiandanSign(examId, blob);
|
||||
if (res.Status !== 200) {
|
||||
throw new Error(res.Message || "提交签名失败");
|
||||
}
|
||||
window.electronAPI?.log("info", "[UI7] 签名提交成功");
|
||||
navigate("/UI8");
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message || "签名提交失败,请稍后重试";
|
||||
window.electronAPI?.log("error", `[UI7] ${msg}`);
|
||||
alert(msg);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
execute();
|
||||
}, [canvasToBlob, isCanvasBlank, isSubmitting, navigate]);
|
||||
|
||||
// 稳定 PDF 页数更新回调
|
||||
const handlePageCountUpdate = useCallback((index: number, numPages: number) => {
|
||||
@@ -206,12 +403,32 @@ const UI7: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="ui7-text_4">请阅读后在下方签名确认</span>
|
||||
|
||||
<div className="ui7-signature-wrapper">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="ui7-signature-canvas"
|
||||
onPointerDown={startDrawing}
|
||||
onPointerMove={draw}
|
||||
onPointerUp={stopDrawing}
|
||||
onPointerLeave={stopDrawing}
|
||||
onPointerCancel={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={isSubmitting ? "提交中..." : "确定"}
|
||||
onClick={handleConfirm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,8 @@ const UI9: React.FC = () => {
|
||||
navigate("/");
|
||||
}, [navigate]);
|
||||
|
||||
const [countdown, setCountdown] = useState(10);
|
||||
const [backTime, setBackTime] = useState("确认(10S)");
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [backTime, setBackTime] = useState("确认(30S)");
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
|
||||
Reference in New Issue
Block a user