Files
ipad/src/components/exam/ExamSignPanel.tsx
2026-01-09 09:15:28 +08:00

2199 lines
76 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import type { OutputTongyishuFileInfo, OutputTijianPdfFileInfo } from '../../api';
import { signInMedicalExamCenter, submitTongyishuSign, submitDaojiandanSign, editDaojiandanPrintStatus, submitAddItemBillSign, getTijianPdfFile, getTongyishuPdf, getDaojiandanPdf } from '../../api';
import type { SignaturePadHandle } from '../ui';
import { Button, SignaturePad } from '../ui';
// Polyfill for Promise.withResolvers
if (typeof (Promise as any).withResolvers === 'undefined') {
(Promise as any).withResolvers = function <T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
}
// 配置 PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
interface ExamSignPanelProps {
examId?: number;
onBusyChange?: (busy: boolean) => void;
}
export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
const [idCardFile, setIdCardFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [showImagePreview, setShowImagePreview] = useState(false);
const [signLoading, setSignLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [consentList, setConsentList] = useState<OutputTongyishuFileInfo[]>([]);
const [consentLoading, setConsentLoading] = useState(false);
const [consentMessage, setConsentMessage] = useState<string | null>(null);
const [previewPdf, setPreviewPdf] = useState<OutputTongyishuFileInfo | null>(null);
const [showSignature, setShowSignature] = useState(false);
const signaturePadRef = useRef<SignaturePadHandle | null>(null);
const [submitLoading, setSubmitLoading] = useState(false);
const [submitMessage, setSubmitMessage] = useState<string | null>(null);
const [signedCombinationCodes, setSignedCombinationCodes] = useState<number[]>([]);
const [pdfData, setPdfData] = useState<ArrayBuffer | null>(null);
const [pdfReady, setPdfReady] = useState(false);
const [pdfLoading, setPdfLoading] = useState(false);
const [pdfBlobUrl, setPdfBlobUrl] = useState<string | null>(null);
const canvasContainerRef = useRef<HTMLDivElement>(null);
// 导检单相关状态
const [daojiandanUrl, setDaojiandanUrl] = useState<string | null>(null);
const [isDaojiandanSigned, setIsDaojiandanSigned] = useState(false); // 导检单是否已签名
const [showDaojiandanSignature, setShowDaojiandanSignature] = useState(false);
const daojiandanSignaturePadRef = useRef<SignaturePadHandle | null>(null);
const [daojiandanSubmitLoading, setDaojiandanSubmitLoading] = useState(false);
const [daojiandanSubmitMessage, setDaojiandanSubmitMessage] = useState<string | null>(null);
const [showDaojiandanPreview, setShowDaojiandanPreview] = useState(false);
const [daojiandanPdfData, setDaojiandanPdfData] = useState<ArrayBuffer | null>(null);
const [daojiandanPdfLoading, setDaojiandanPdfLoading] = useState(false);
const [daojiandanPdfReady, setDaojiandanPdfReady] = useState(false);
const [daojiandanPdfBlobUrl, setDaojiandanPdfBlobUrl] = useState<string | null>(null);
const daojiandanCanvasContainerRef = useRef<HTMLDivElement>(null);
const [showAddItemBillPreview, setShowAddItemBillPreview] = useState(false);
const [showAddItemBillSignature, setShowAddItemBillSignature] = useState(false);
const addItemBillSignaturePadRef = useRef<SignaturePadHandle | null>(null);
const [addItemBillSubmitLoading, setAddItemBillSubmitLoading] = useState(false);
const [addItemBillSubmitMessage, setAddItemBillSubmitMessage] = useState<string | null>(null);
type AddItemBillItem = {
pdf_sort: number;
combinationCode: string;
payment_status?: string | null;
payment_status_name?: string | null;
pdf_name: string;
pdf_url: string;
is_signed: boolean;
};
const [addItemBillList, setAddItemBillList] = useState<AddItemBillItem[]>([]);
const [currentAddItemBill, setCurrentAddItemBill] = useState<AddItemBillItem | null>(null);
const [addItemBillPdfData, setAddItemBillPdfData] = useState<ArrayBuffer | null>(null);
const [addItemBillPdfLoading, setAddItemBillPdfLoading] = useState(false);
const [addItemBillPdfReady, setAddItemBillPdfReady] = useState(false);
const [addItemBillPdfBlobUrl, setAddItemBillPdfBlobUrl] = useState<string | null>(null);
const addItemBillCanvasContainerRef = useRef<HTMLDivElement>(null);
const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading || addItemBillSubmitLoading;
useEffect(() => {
onBusyChange?.(busy);
return () => onBusyChange?.(false);
}, [busy, onBusyChange]);
const refreshTijianPdfs = async (examIdValue: number) => {
setConsentLoading(true);
setConsentMessage(null);
try {
const res = await getTijianPdfFile({ exam_id: examIdValue });
const list: OutputTijianPdfFileInfo[] = res.Data || [];
// 知情同意书列表pdf_type = 2
const consentItems = list.filter((item) => item.pdf_type === 2);
const mappedConsent: OutputTongyishuFileInfo[] = consentItems.map((item) => ({
pdf_name: item.pdf_name || '',
pdf_url: item.sign_pdf_url || item.pdf_url || '',
combination_code: item.combination_code ?? null,
is_signed: item.is_sign === 1,
}));
setConsentList(mappedConsent);
// 已签名的组合码,用于一键打印等逻辑
const signedCodes =
consentItems
.filter((item) => item.is_sign === 1 && item.combination_code !== null && item.combination_code !== undefined)
.map((item) => Number(item.combination_code))
.filter((n) => Number.isFinite(n)) || [];
setSignedCombinationCodes(signedCodes);
if (!mappedConsent.length) {
setConsentMessage(res.Message || '暂无知情同意书');
}
// 导检单pdf_type = 1取第一条
const daojiandan = list.find((item) => item.pdf_type === 1);
if (daojiandan) {
const url = daojiandan.sign_pdf_url || daojiandan.pdf_url || null;
setDaojiandanUrl(url);
setIsDaojiandanSigned(daojiandan.is_sign === 1);
} else {
// 如果 getTijianPdfFile 没有返回导检单,保留之前通过 getDaojiandanPdf 获取的URL如果存在
// 不设置为 null这样未签名的导检单也能显示"查看"按钮
setDaojiandanUrl((prev) => (prev !== null ? prev : null));
setIsDaojiandanSigned(false);
}
// 加项单pdf_type = 3完全由新接口提供列表和签名状态
const addItemFromApi = list.filter((item) => item.pdf_type === 3);
if (addItemFromApi.length > 0) {
const addItemList: AddItemBillItem[] = addItemFromApi.map((item) => ({
pdf_sort: item.combination_code ?? 0,
combinationCode: String(item.combination_code ?? ''),
payment_status: item.is_pay != null ? String(item.is_pay) : null,
payment_status_name: item.is_pay_name ?? null,
pdf_name: item.pdf_name || '加项单',
pdf_url: item.sign_pdf_url || item.pdf_url || '',
is_signed: item.is_sign === 1,
}));
setAddItemBillList(addItemList);
const unsigned = addItemList.find((bill) => bill.is_signed !== true);
setCurrentAddItemBill(unsigned || addItemList[0] || null);
} else {
setAddItemBillList([]);
setCurrentAddItemBill(null);
}
} catch (err) {
console.error('获取体检PDF列表失败', err);
setConsentMessage('知情同意书加载失败,请稍后重试');
} finally {
setConsentLoading(false);
}
};
// 初始化加载体检 PDF 列表(导检单、知情同意书、加项单)
useEffect(() => {
if (!examId) {
setConsentList([]);
setConsentMessage('缺少体检ID无法获取知情同意书');
setDaojiandanUrl(null);
setIsDaojiandanSigned(false);
return;
}
// 初始化时加载知情同意书和导检单
const loadTongyishu = async () => {
try {
const res = await getTongyishuPdf({ exam_id: examId });
if (res.Status === 200 && res.Data?.list_pdf_url) {
const list = res.Data.list_pdf_url;
const mappedConsent: OutputTongyishuFileInfo[] = list.map((item) => ({
pdf_name: item.pdf_name || '',
pdf_url: item.pdf_url || '',
combination_code: item.combination_code ?? null,
}));
setConsentList(mappedConsent);
if (mappedConsent.length === 0) {
setConsentMessage(res.Message || '暂无知情同意书');
}
}
} catch (err) {
console.error('获取知情同意书失败', err);
}
};
// 初始化时加载导检单(未签名版本也可以查看)
const loadDaojiandan = async () => {
try {
const res = await getDaojiandanPdf({ exam_id: examId });
if (res.Status === 200 && res.Data?.pdf_url) {
// 如果 refreshTijianPdfs 没有设置导检单URL则使用这里获取的未签名版本
setDaojiandanUrl((prev) => prev || res.Data?.pdf_url || null);
}
} catch (err) {
console.error('获取导检单失败', err);
}
};
// 先加载知情同意书和导检单然后刷新所有PDF状态包括签名状态
Promise.all([loadTongyishu(), loadDaojiandan()]).then(() => {
refreshTijianPdfs(examId);
});
}, [examId]);
const handlePickFile = () => {
fileInputRef.current?.click();
};
const convertToJpg = async (file: File): Promise<File> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法创建画布上下文'));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('图片转换失败'));
return;
}
const jpgFile = new File([blob], 'id_card.jpg', { type: 'image/jpeg' });
resolve(jpgFile);
},
'image/jpeg',
0.92
);
};
img.onerror = () => reject(new Error('图片加载失败'));
img.src = event.target?.result as string;
};
reader.onerror = () => reject(new Error('文件读取失败'));
reader.readAsDataURL(file);
});
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setMessage(null);
try {
const jpgFile = await convertToJpg(file);
setIdCardFile(jpgFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(jpgFile);
} catch (err) {
console.error('图片转换失败', err);
setMessage('图片处理失败,请重试');
}
e.target.value = '';
};
const handleSign = async () => {
if (!idCardFile) {
setMessage('请先上传身份证照片');
return;
}
setSignLoading(true);
setMessage(null);
try {
const res = await signInMedicalExamCenter({ id_no_pic: idCardFile });
const ok = res.Status === 200 && res.Data?.is_success === 0;
if (ok) {
setMessage('签到成功');
} else {
setMessage(res.Message || '签到失败');
}
} catch (err) {
console.error(err);
setMessage('签到请求失败,请稍后重试');
} finally {
setSignLoading(false);
}
};
const handleSubmitSign = async () => {
if (!examId || !previewPdf?.combination_code) {
setSubmitMessage('缺少必要信息,无法提交签名');
return;
}
const dataUrl = signaturePadRef.current?.toDataURL('image/png');
if (!dataUrl) {
setSubmitMessage('请先完成签名');
return;
}
setSubmitLoading(true);
setSubmitMessage(null);
try {
const blob = await fetch(dataUrl).then((r) => r.blob());
const res = await submitTongyishuSign({
exam_id: examId,
combination_code: previewPdf.combination_code,
sign_file: blob,
});
if (res.Status === 200) {
setSubmitMessage('签名提交成功');
// 更新本地已签名组合码(立刻刷新按钮状态)
setSignedCombinationCodes((prev) => {
const code = Number(previewPdf.combination_code);
if (!Number.isFinite(code)) return prev || [];
return Array.from(new Set([...(prev || []), code]));
});
setTimeout(() => {
setShowSignature(false);
setPreviewPdf(null);
setSubmitMessage(null);
signaturePadRef.current?.clear();
// 更新签名状态(刷新统一 PDF 列表)
if (examId) {
refreshTijianPdfs(examId);
}
}, 500);
} else {
setSubmitMessage(res.Message || '签名提交失败');
}
} catch (err) {
console.error('提交签名失败', err);
setSubmitMessage('签名提交失败,请稍后重试');
} finally {
setSubmitLoading(false);
}
};
// 加载 PDF 数据
useEffect(() => {
if (!previewPdf?.pdf_url) {
setPdfData(null);
setPdfBlobUrl(null);
setPdfReady(false);
return;
}
let objectUrl: string | null = null;
setPdfReady(false);
setPdfLoading(true);
setPdfData(null);
fetch(previewPdf.pdf_url)
.then((resp) => {
if (!resp.ok) throw new Error('获取PDF文件失败');
return resp.blob();
})
.then((blob) => {
objectUrl = URL.createObjectURL(blob);
setPdfBlobUrl(objectUrl);
return blob.arrayBuffer();
})
.then((arrayBuffer) => {
setPdfData(arrayBuffer);
setPdfLoading(false);
})
.catch((err) => {
console.error('PDF 拉取失败', err);
setPdfLoading(false);
});
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [previewPdf?.pdf_url]);
// 渲染 PDF
useEffect(() => {
if (!pdfData || !canvasContainerRef.current) return;
setPdfReady(false);
const renderAllPages = async () => {
try {
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
if (!canvasContainerRef.current) return;
// 清空容器
canvasContainerRef.current.innerHTML = '';
const scale = 3;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.display = 'block';
canvas.style.marginBottom = '10px';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
canvas.className = 'mx-auto border rounded-lg shadow-sm';
canvasContainerRef.current.appendChild(canvas);
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
}
setPdfReady(true);
} catch (err) {
console.error('PDF 渲染失败', err);
setPdfLoading(false);
}
};
renderAllPages();
}, [pdfData]);
// 加载导检单 PDF 数据
useEffect(() => {
if (!showDaojiandanPreview || !daojiandanUrl) {
setDaojiandanPdfData(null);
setDaojiandanPdfBlobUrl(null);
setDaojiandanPdfReady(false);
return;
}
let objectUrl: string | null = null;
setDaojiandanPdfReady(false);
setDaojiandanPdfLoading(true);
setDaojiandanPdfData(null);
fetch(daojiandanUrl)
.then((resp) => {
if (!resp.ok) throw new Error('获取PDF文件失败');
return resp.blob();
})
.then((blob) => {
objectUrl = URL.createObjectURL(blob);
setDaojiandanPdfBlobUrl(objectUrl);
return blob.arrayBuffer();
})
.then((arrayBuffer) => {
setDaojiandanPdfData(arrayBuffer);
setDaojiandanPdfLoading(false);
})
.catch((err) => {
console.error('导检单PDF 拉取失败', err);
setDaojiandanPdfLoading(false);
});
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [showDaojiandanPreview, daojiandanUrl]);
// 渲染导检单 PDF
useEffect(() => {
if (!daojiandanPdfData || !daojiandanCanvasContainerRef.current) return;
setDaojiandanPdfReady(false);
const renderAllPages = async () => {
try {
const pdf = await pdfjsLib.getDocument({ data: daojiandanPdfData }).promise;
if (!daojiandanCanvasContainerRef.current) return;
// 清空容器
daojiandanCanvasContainerRef.current.innerHTML = '';
const scale = 3.0;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.display = 'block';
canvas.style.marginBottom = '10px';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
canvas.className = 'mx-auto border rounded-lg shadow-sm';
daojiandanCanvasContainerRef.current.appendChild(canvas);
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
}
setDaojiandanPdfReady(true);
} catch (err) {
console.error('导检单PDF 渲染失败', err);
setDaojiandanPdfLoading(false);
}
};
renderAllPages();
}, [daojiandanPdfData]);
// 加载加项单 PDF 数据(当前选中的加项单)
useEffect(() => {
if (!showAddItemBillPreview || !currentAddItemBill?.pdf_url) {
setAddItemBillPdfData(null);
setAddItemBillPdfBlobUrl(null);
setAddItemBillPdfReady(false);
return;
}
let objectUrl: string | null = null;
setAddItemBillPdfReady(false);
setAddItemBillPdfLoading(true);
setAddItemBillPdfData(null);
fetch(currentAddItemBill.pdf_url)
.then((resp) => {
if (!resp.ok) throw new Error('获取PDF文件失败');
return resp.blob();
})
.then((blob) => {
objectUrl = URL.createObjectURL(blob);
setAddItemBillPdfBlobUrl(objectUrl);
return blob.arrayBuffer();
})
.then((arrayBuffer) => {
setAddItemBillPdfData(arrayBuffer);
setAddItemBillPdfLoading(false);
})
.catch((err) => {
console.error('加项单PDF 拉取失败', err);
setAddItemBillPdfLoading(false);
});
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [showAddItemBillPreview, currentAddItemBill?.pdf_url]);
// 渲染加项单 PDF
useEffect(() => {
if (!addItemBillPdfData || !addItemBillCanvasContainerRef.current) return;
setAddItemBillPdfReady(false);
const renderAllPages = async () => {
try {
const pdf = await pdfjsLib.getDocument({ data: addItemBillPdfData }).promise;
if (!addItemBillCanvasContainerRef.current) return;
// 清空容器
addItemBillCanvasContainerRef.current.innerHTML = '';
const scale = 3.0;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.display = 'block';
canvas.style.marginBottom = '10px';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
canvas.className = 'mx-auto border rounded-lg shadow-sm';
addItemBillCanvasContainerRef.current.appendChild(canvas);
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
}
setAddItemBillPdfReady(true);
} catch (err) {
console.error('加项单PDF 渲染失败', err);
setAddItemBillPdfLoading(false);
}
};
renderAllPages();
}, [addItemBillPdfData]);
// 导检单预览:使用 pdfjs 渲染到 canvas
useEffect(() => {
if (!showDaojiandanPreview || !daojiandanUrl || !daojiandanCanvasContainerRef.current) return;
const container = daojiandanCanvasContainerRef.current;
let cancelled = false;
const renderDaojiandan = async () => {
try {
setDaojiandanPdfLoading(true);
container.innerHTML = '';
const resp = await fetch(daojiandanUrl);
if (!resp.ok) throw new Error('获取PDF文件失败');
const blob = await resp.blob();
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const scale = 3.0;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
if (cancelled) return;
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.display = 'block';
canvas.style.marginBottom = '10px';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
canvas.className = 'mx-auto border rounded-lg shadow-sm';
container.appendChild(canvas);
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
}
} catch (err) {
console.error('导检单PDF 渲染失败', err);
} finally {
if (!cancelled) {
setDaojiandanPdfLoading(false);
}
}
};
renderDaojiandan();
return () => {
cancelled = true;
container.innerHTML = '';
};
}, [showDaojiandanPreview, daojiandanUrl]);
// 加项单预览:使用 pdfjs 渲染到 canvas依赖加载好的 addItemBillPdfData
useEffect(() => {
if (!showAddItemBillPreview || !addItemBillPdfData || !addItemBillCanvasContainerRef.current) return;
const container = addItemBillCanvasContainerRef.current;
container.innerHTML = '';
const renderAddItemBill = async () => {
try {
const pdf = await pdfjsLib.getDocument({ data: addItemBillPdfData }).promise;
const scale = 3.0;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
canvas.style.display = 'block';
canvas.style.marginBottom = '10px';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
canvas.className = 'mx-auto border rounded-lg shadow-sm';
container.appendChild(canvas);
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
}
} catch (err) {
console.error('加项单PDF 渲染失败', err);
}
};
renderAddItemBill();
return () => {
container.innerHTML = '';
};
}, [showAddItemBillPreview, addItemBillPdfData]);
const handlePrint = () => {
if (!pdfBlobUrl || !pdfReady || !canvasContainerRef.current || !previewPdf) return;
// 创建打印窗口
const printWindow = window.open('', '_blank');
if (!printWindow) return;
// 获取所有的 canvas
const canvases = canvasContainerRef.current.querySelectorAll('canvas');
// 构建打印页面
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${previewPdf.pdf_name || '知情同意书打印'}</title>
<style>
@media print {
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
display: none;
}
img {
max-width: 100%;
page-break-after: always;
page-break-inside: avoid;
}
img:last-child {
page-break-after: auto;
}
}
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.print-button {
background-color: #2563eb;
color: white;
}
.print-button:hover {
background-color: #1d4ed8;
}
.close-button {
background-color: transparent;
color: #374151;
}
.close-button:hover {
background-color: #e5e7eb;
}
.button-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
}
img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="button-container">
<button class="close-button" onclick="window.close()">关闭</button>
<button class="print-button" onclick="window.print()">打印</button>
</div>
`);
// 将每个 canvas 转换为图片并添加到打印窗口
canvases.forEach((canvas) => {
const imgData = (canvas as HTMLCanvasElement).toDataURL('image/png');
printWindow.document.write(`<img src="${imgData}" />`);
});
printWindow.document.write(`
</body>
</html>
`);
printWindow.document.close();
// 等待图片加载完成后执行打印
printWindow.onload = () => {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 1000);
};
};
const handleDirectPrint = async (pdfItem: OutputTongyishuFileInfo) => {
if (!pdfItem.pdf_url) return;
try {
// 加载PDF
const response = await fetch(pdfItem.pdf_url);
if (!response.ok) throw new Error('获取PDF文件失败');
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
// 渲染PDF到临时canvas
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const scale = 1.0;
const canvasImages: string[] = [];
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
canvasImages.push(canvas.toDataURL('image/png'));
}
// 创建打印窗口
const printWindow = window.open('', '_blank');
if (!printWindow) return;
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${pdfItem.pdf_name || '知情同意书打印'}</title>
<style>
@media print {
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
display: none;
}
img {
max-width: 100%;
page-break-after: always;
page-break-inside: avoid;
}
img:last-child {
page-break-after: auto;
}
}
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.print-button {
background-color: #2563eb;
color: white;
}
.print-button:hover {
background-color: #1d4ed8;
}
.close-button {
background-color: transparent;
color: #374151;
}
.close-button:hover {
background-color: #e5e7eb;
}
.button-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
}
img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="button-container">
<button class="close-button" onclick="window.close()">关闭</button>
<button class="print-button" onclick="window.print()">打印</button>
</div>
`);
canvasImages.forEach((imgData) => {
printWindow.document.write(`<img src="${imgData}" />`);
});
printWindow.document.write(`
</body>
</html>
`);
printWindow.document.close();
// 等待图片加载完成后执行打印
printWindow.onload = () => {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 1000);
};
} catch (err) {
console.error('打印失败', err);
alert('打印失败,请稍后重试');
}
};
// 导检单签名提交
const handleSubmitDaojiandanSign = async () => {
if (!examId) {
setDaojiandanSubmitMessage('缺少必要信息,无法提交签名');
return;
}
const dataUrl = daojiandanSignaturePadRef.current?.toDataURL('image/png');
if (!dataUrl) {
setDaojiandanSubmitMessage('请先完成签名');
return;
}
setDaojiandanSubmitLoading(true);
setDaojiandanSubmitMessage(null);
try {
const blob = await fetch(dataUrl).then((r) => r.blob());
const res = await submitDaojiandanSign({
exam_id: examId,
sign_file: blob,
});
if (res.Status === 200 && res.Data?.pdf_url) {
setDaojiandanSubmitMessage('签名提交成功');
const pdfUrlValue = res.Data.pdf_url;
const pdfNameValue = res.Data.pdf_name || '导检单';
setDaojiandanUrl(pdfUrlValue);
setIsDaojiandanSigned(true); // 签名成功后标记为已签名
// 更新导检单打印状态
try {
await editDaojiandanPrintStatus({ exam_id: examId });
} catch (err) {
console.error('更新导检单打印状态失败', err);
}
setTimeout(() => {
setShowDaojiandanSignature(false);
setDaojiandanSubmitMessage(null);
daojiandanSignaturePadRef.current?.clear();
setShowDaojiandanPreview(true);
}, 500);
} else {
setDaojiandanSubmitMessage(res.Message || '签名提交失败');
}
} catch (err) {
console.error('提交签名失败', err);
setDaojiandanSubmitMessage('签名提交失败,请稍后重试');
} finally {
setDaojiandanSubmitLoading(false);
}
};
// 加项单签名提交(针对当前选中的加项单)
const handleSubmitAddItemBillSign = async () => {
if (!examId) {
setAddItemBillSubmitMessage('缺少必要信息,无法提交签名');
return;
}
if (!currentAddItemBill) {
setAddItemBillSubmitMessage('没有可签名的加项单');
return;
}
const dataUrl = addItemBillSignaturePadRef.current?.toDataURL('image/png');
if (!dataUrl) {
setAddItemBillSubmitMessage('请先完成签名');
return;
}
// 记录当前正在签名的加项单,用于签名成功后重新选中它
const justSignedCombinationCode = currentAddItemBill.combinationCode;
setAddItemBillSubmitLoading(true);
setAddItemBillSubmitMessage(null);
try {
const blob = await fetch(dataUrl).then((r) => r.blob());
const res = await submitAddItemBillSign({
exam_id: examId,
pdf_sort: currentAddItemBill.pdf_sort,
combinationCode: currentAddItemBill.combinationCode,
sign_file: blob,
});
if (res.Status === 200) {
setAddItemBillSubmitMessage('签名提交成功');
// 签名成功后刷新统一 PDF 列表,获取最新的加项单签名状态和地址
if (examId) {
try {
const refreshRes = await getTijianPdfFile({ exam_id: examId as number });
const list: OutputTijianPdfFileInfo[] = refreshRes.Data || [];
const addItemFromApi = list.filter((item) => item.pdf_type === 3);
if (addItemFromApi.length > 0) {
const addItemList: AddItemBillItem[] = addItemFromApi.map((item) => ({
pdf_sort: item.combination_code ?? 0,
combinationCode: String(item.combination_code ?? ''),
payment_status: item.is_pay != null ? String(item.is_pay) : null,
payment_status_name: item.is_pay_name ?? null,
pdf_name: item.pdf_name || '加项单',
pdf_url: item.sign_pdf_url || item.pdf_url || '',
is_signed: item.is_sign === 1,
}));
setAddItemBillList(addItemList);
// 优先选中刚刚签名的那一张,其次选未签名的,其次第一张
const justSigned = addItemList.find(
(bill) => bill.combinationCode === justSignedCombinationCode
);
const unsigned = addItemList.find((bill) => bill.is_signed !== true);
setCurrentAddItemBill(justSigned || unsigned || addItemList[0] || null);
} else {
setAddItemBillList([]);
setCurrentAddItemBill(null);
}
} catch (e) {
console.error('刷新加项单列表失败', e);
}
}
setTimeout(() => {
setShowAddItemBillSignature(false);
setAddItemBillSubmitMessage(null);
addItemBillSignaturePadRef.current?.clear();
setShowAddItemBillPreview(true);
}, 500);
} else {
setAddItemBillSubmitMessage(res.Message || '签名提交失败');
}
} catch (err) {
console.error('提交签名失败', err);
setAddItemBillSubmitMessage('签名提交失败,请稍后重试');
} finally {
setAddItemBillSubmitLoading(false);
}
};
// 加项单直接打印
const handleAddItemBillDirectPrint = async (target: AddItemBillItem | null) => {
if (!target?.pdf_url) return;
try {
const response = await fetch(target.pdf_url);
if (!response.ok) throw new Error('获取PDF文件失败');
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const scale = 3.0;
const canvasImages: string[] = [];
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
canvasImages.push(canvas.toDataURL('image/png'));
}
const printWindow = window.open('', '_blank');
if (!printWindow) return;
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${target.pdf_name || '加项单打印'}</title>
<style>
@media print {
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
display: none;
}
img {
max-width: 100%;
page-break-after: always;
page-break-inside: avoid;
}
img:last-child {
page-break-after: auto;
}
}
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.print-button {
background-color: #2563eb;
color: white;
}
.print-button:hover {
background-color: #1d4ed8;
}
.close-button {
background-color: transparent;
color: #374151;
}
.close-button:hover {
background-color: #e5e7eb;
}
.button-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
}
img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="button-container">
<button class="close-button" onclick="window.close()">关闭</button>
<button class="print-button" onclick="window.print()">打印</button>
</div>
`);
canvasImages.forEach((imgData) => {
printWindow.document.write(`<img src="${imgData}" />`);
});
printWindow.document.write(`
</body>
</html>
`);
printWindow.document.close();
printWindow.onload = () => {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 1000);
};
} catch (err) {
console.error('打印失败', err);
alert('打印失败,请稍后重试');
}
};
// 导检单直接打印
const handleDaojiandanDirectPrint = async () => {
if (!daojiandanUrl) return;
try {
const response = await fetch(daojiandanUrl);
if (!response.ok) throw new Error('获取PDF文件失败');
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const scale = 3.0;
const canvasImages: string[] = [];
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
canvasImages.push(canvas.toDataURL('image/png'));
}
const printWindow = window.open('', '_blank');
if (!printWindow) return;
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>导检单打印</title>
<style>
@media print {
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
display: none;
}
img {
max-width: 100%;
page-break-after: always;
page-break-inside: avoid;
}
img:last-child {
page-break-after: auto;
}
}
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.print-button {
background-color: #2563eb;
color: white;
}
.print-button:hover {
background-color: #1d4ed8;
}
.close-button {
background-color: transparent;
color: #374151;
}
.close-button:hover {
background-color: #e5e7eb;
}
.button-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
}
img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="button-container">
<button class="close-button" onclick="window.close()">关闭</button>
<button class="print-button" onclick="window.print()">打印</button>
</div>
`);
canvasImages.forEach((imgData) => {
printWindow.document.write(`<img src="${imgData}" />`);
});
printWindow.document.write(`
</body>
</html>
`);
// 在关闭文档前设置 onload 事件
printWindow.onload = () => {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 1000);
};
printWindow.document.close();
// 更新导检单打印状态(不阻塞打印流程)
if (examId) {
editDaojiandanPrintStatus({ exam_id: examId }).catch((err) => {
console.error('更新导检单打印状态失败', err);
});
}
} catch (err) {
console.error('打印失败', err);
alert('打印失败,请稍后重试');
}
};
// 检查所有文档是否都已签名
const checkAllSigned = () => {
// 检查所有知情同意书是否都已签名
const allConsentsSigned = consentList.every((item) => {
if (item.combination_code === undefined || item.combination_code === null) return true;
return signedCombinationCodes.includes(Number(item.combination_code));
});
// 检查导检单是否已签名(只有 localStorage 中的或签名后的才是已签名的)
const daojiandanSigned = isDaojiandanSigned;
return allConsentsSigned && consentList.length > 0 && daojiandanSigned;
};
// 一键打印所有文档
const handleBatchPrint = async () => {
if (busy) return;
try {
const allImages: string[] = [];
const scale = 3.0;
// 处理所有已签名的知情同意书
for (const item of consentList) {
if (item.combination_code === undefined || item.combination_code === null) continue;
if (!signedCombinationCodes.includes(Number(item.combination_code))) continue;
const targetUrl = item.pdf_url;
if (!targetUrl) continue;
try {
const response = await fetch(targetUrl);
if (!response.ok) continue;
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
allImages.push(canvas.toDataURL('image/png'));
}
} catch (err) {
console.error(`处理知情同意书 ${item.pdf_name} 失败`, err);
}
}
// 处理导检单
if (daojiandanUrl) {
try {
const response = await fetch(daojiandanUrl);
if (response.ok) {
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
allImages.push(canvas.toDataURL('image/png'));
}
}
} catch (err) {
console.error('处理导检单失败', err);
}
}
// 处理加项单(如果有且已签名)
if (addItemBillList.length > 0) {
try {
for (const bill of addItemBillList) {
if (!bill.pdf_url || bill.is_signed !== true) continue;
const response = await fetch(bill.pdf_url);
if (response.ok) {
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) continue;
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
} as any;
await page.render(renderContext).promise;
allImages.push(canvas.toDataURL('image/png'));
}
}
}
} catch (err) {
console.error('处理加项单失败', err);
}
}
if (allImages.length === 0) {
alert('没有可打印的内容');
return;
}
// 创建打印窗口
const printWindow = window.open('', '_blank');
if (!printWindow) {
alert('无法打开打印窗口,请检查浏览器弹窗设置');
return;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>一键打印</title>
<style>
@media print {
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
display: none;
}
img {
max-width: 100%;
page-break-after: always;
page-break-inside: avoid;
}
img:last-child {
page-break-after: auto;
}
}
body {
margin: 0;
padding: 0;
}
.print-button, .close-button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.print-button {
background-color: #2563eb;
color: white;
}
.print-button:hover {
background-color: #1d4ed8;
}
.close-button {
background-color: transparent;
color: #374151;
}
.close-button:hover {
background-color: #e5e7eb;
}
.button-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
}
img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="button-container">
<button class="close-button" onclick="window.close()">关闭</button>
<button class="print-button" onclick="window.print()">打印</button>
</div>
`);
allImages.forEach((imgData) => {
printWindow.document.write(`<img src="${imgData}" />`);
});
printWindow.document.write(`
</body>
</html>
`);
printWindow.document.close();
// 如果包含导检单,更新导检单打印状态
if (examId && daojiandanUrl) {
try {
await editDaojiandanPrintStatus({ exam_id: examId });
} catch (err) {
console.error('更新导检单打印状态失败', err);
// 不阻塞打印流程,仅记录错误
}
}
// 等待图片加载完成后自动弹出打印窗口
printWindow.onload = () => {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 1000);
};
} catch (err) {
console.error('一键打印失败', err);
alert('一键打印失败,请稍后重试');
}
};
return (
<div className='grid grid-cols-2 gap-4 text-sm'>
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
<div className='font-medium'></div>
<div className='text-xs text-gray-500'></div>
<div className='flex items-center gap-3'>
<Button className='py-1.5 px-3' onClick={handlePickFile} disabled={busy}>
{idCardFile ? '重新拍照' : '拍照'}
</Button>
<input
ref={fileInputRef}
type='file'
accept='image/*'
capture='environment'
className='hidden'
onChange={handleFileChange}
/>
{previewImage && (
<div
className='w-16 h-16 rounded-lg border-2 border-gray-300 overflow-hidden cursor-pointer hover:border-blue-500 transition-colors'
onClick={() => setShowImagePreview(true)}
>
<img src={previewImage} alt='身份证预览' className='w-full h-full object-cover' />
</div>
)}
<Button className='py-1.5 px-4 flex-1' onClick={handleSign} disabled={busy || !idCardFile}>
{signLoading ? '签到中...' : '签到'}
</Button>
</div>
{message && (
<div className={`text-xs ${message.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>{message}</div>
)}
</div>
{showImagePreview && previewImage && (
<div className='fixed inset-0 z-[80] bg-black/90 flex items-center justify-center p-6' onClick={() => setShowImagePreview(false)}>
<div className='relative max-w-full max-h-full'>
<img src={previewImage} alt='身份证预览' className='max-w-full max-h-[90vh] object-contain' />
<Button className='absolute top-4 right-4 bg-white/90 hover:bg-white' onClick={() => setShowImagePreview(false)}>
</Button>
</div>
</div>
)}
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
<div className='flex items-center justify-between'>
<div className='font-medium'></div>
{checkAllSigned() && (
<Button
className='py-1 px-3 bg-green-600 hover:bg-green-700 text-white text-xs'
onClick={handleBatchPrint}
disabled={busy}
>
</Button>
)}
</div>
{/* <div className='text-xs text-gray-500'>点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。</div> */}
<div className='flex flex-col gap-2 max-h-96 overflow-y-auto custom-scroll'>
{consentLoading && <div className='text-xs text-gray-500'>...</div>}
{/* {!consentLoading && consentMessage && <div className='text-xs text-amber-600'>{consentMessage}</div>} */}
{!consentLoading && (consentList.length > 0 || true) && (
<div className='space-y-2'>
{consentList.map((item) => (
<div
key={item.pdf_url || item.pdf_name}
className='flex items-center justify-between gap-3 p-2 rounded-xl border bg-white shadow-sm'
>
<div className='text-sm text-gray-800 truncate flex items-center gap-2 relative pr-20'>
<span className='truncate'>{item.pdf_name.length > 12 ? item.pdf_name.slice(0, 12) + "..." : item.pdf_name}</span>
{item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && (
<img
src='/sign.png'
alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy'
/>
)}
</div>
<div className='flex items-center gap-2'>
{item.combination_code !== undefined && signedCombinationCodes.includes(Number(item.combination_code)) && (
<Button
className='py-1.5 px-3 bg-blue-600 hover:bg-blue-700 text-white'
onClick={() => {
if (busy) return;
handleDirectPrint(item);
}}
disabled={busy}
>
</Button>
)}
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setPreviewPdf(item);
}}
disabled={busy}
>
</Button>
</div>
</div>
))}
<div className='flex items-center justify-between gap-3 p-2 rounded-xl border bg-white shadow-sm'>
<div className='text-sm text-gray-800 truncate flex items-center gap-2 relative pr-20'>
<span className='truncate'></span>
{isDaojiandanSigned && (
<img
src='/sign.png'
alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy'
/>
)}
</div>
<div className='flex items-center gap-2'>
{isDaojiandanSigned ? (
// 已签名:显示打印和查看按钮
<>
<Button
className='py-1.5 px-3 bg-blue-600 hover:bg-blue-700 text-white'
onClick={() => {
if (busy) return;
handleDaojiandanDirectPrint();
}}
disabled={busy}
>
</Button>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setShowDaojiandanPreview(true);
}}
disabled={busy}
>
</Button>
</>
) : daojiandanUrl ? (
// 未签名但有导检单:显示查看和签名按钮
<>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setShowDaojiandanPreview(true);
}}
disabled={busy}
>
</Button>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setShowDaojiandanSignature(true);
}}
disabled={busy}
>
</Button>
</>
) : (
// 没有导检单:只显示签名按钮
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setShowDaojiandanSignature(true);
}}
disabled={busy}
>
</Button>
)}
</div>
</div>
{/* 加项单列表(可能有多个) */}
{addItemBillList.length > 0 &&
addItemBillList.map((bill) => {
const isSigned = bill.is_signed === true;
const displayName =
bill.pdf_name && bill.pdf_name.length > 12 ? bill.pdf_name.slice(0, 12) + '...' : bill.pdf_name || '加项单';
return (
<div
key={bill.pdf_sort}
className='flex items-center justify-between gap-3 p-2 rounded-xl border bg-white shadow-sm'
>
<div className='text-sm text-gray-800 truncate flex items-center gap-2 relative pr-20'>
<span className='truncate'>
{displayName}
{typeof bill.pdf_sort === 'number' ? `#${bill.pdf_sort}` : ''}
</span>
{isSigned && (
<img
src='/sign.png'
alt='已签名'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
loading='lazy'
/>
)}
</div>
<div className='flex items-center gap-2'>
{isSigned && bill.pdf_url && (
<Button
className='py-1.5 px-3 bg-blue-600 hover:bg-blue-700 text-white'
onClick={() => {
if (busy) return;
handleAddItemBillDirectPrint(bill);
}}
disabled={busy}
>
</Button>
)}
{bill.pdf_url && (
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setCurrentAddItemBill(bill);
setShowAddItemBillPreview(true);
}}
disabled={busy}
>
</Button>
)}
{!isSigned && (
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setCurrentAddItemBill(bill);
setShowAddItemBillSignature(true);
}}
disabled={busy}
>
</Button>
)}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
{
previewPdf && (
<div className='fixed inset-0 z-[60] bg-black/75 flex flex-col'>
<div className='flex items-center justify-between p-4 text-white bg-gray-900/80'>
<div className='text-sm font-medium truncate pr-3'>{previewPdf.pdf_name}</div>
<div className='flex items-center gap-2'>
<Button
className='py-1 px-3 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed'
onClick={handlePrint}
disabled={pdfLoading || !pdfReady || !pdfBlobUrl}
>
</Button>
<Button className='py-1 px-3' onClick={() => !busy && setShowSignature(true)} disabled={busy}>
</Button>
<Button className='py-1 px-3' onClick={() => !busy && setPreviewPdf(null)} disabled={busy}>
</Button>
</div>
</div>
<div className='flex-1 bg-gray-100 overflow-auto'>
{pdfLoading ? (
<div className='flex items-center justify-center h-full text-white'>
<div>PDF...</div>
</div>
) : pdfData ? (
<div className='flex justify-center p-4'>
<div ref={canvasContainerRef} className='w-full' />
</div>
) : (
<div className='flex items-center justify-center h-full text-white'>
<div>PDF加载失败</div>
</div>
)}
</div>
</div>
)
}
{
showSignature && (
<div className='fixed inset-0 z-[70] bg-black/80 flex items-center justify-center px-6'>
<div className='bg-white rounded-2xl w-full max-w-3xl shadow-2xl p-4 flex flex-col gap-4'>
<div className='flex items-center justify-between'>
<div className='text-base font-semibold text-gray-900'></div>
<div className='flex items-center gap-2'>
<Button className='py-1 px-3' onClick={() => setShowSignature(false)} disabled={busy}>
</Button>
</div>
</div>
<div className='ui7-signature-wrapper border rounded-xl overflow-hidden bg-gray-50'>
<SignaturePad
ref={signaturePadRef}
className='ui7-signature-canvas w-full h-72 bg-white touch-none'
/>
<div className='flex items-center justify-between px-3 py-2 bg-gray-50 border-t'>
<div className='text-xs text-gray-500'></div>
<Button className='ui7-clear-button py-1 px-3' onClick={() => signaturePadRef.current?.clear()} disabled={submitLoading}>
</Button>
</div>
</div>
{submitMessage && (
<div className={`text-sm text-center ${submitMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>{submitMessage}</div>
)}
<div className='flex items-center justify-end gap-3'>
<Button
className='py-2 px-6'
onClick={() => {
setShowSignature(false);
setSubmitMessage(null);
signaturePadRef.current?.clear();
}}
disabled={submitLoading}
>
</Button>
<Button
className='py-2 px-6 bg-blue-600 text-white hover:bg-blue-700'
onClick={handleSubmitSign}
disabled={submitLoading}
>
{submitLoading ? '提交中...' : '提交签名'}
</Button>
</div>
</div>
</div>
)
}
{
showDaojiandanSignature && (
<div className='fixed inset-0 z-[70] bg-black/80 flex items-center justify-center px-6'>
<div className='bg-white rounded-2xl w-full max-w-3xl shadow-2xl p-4 flex flex-col gap-4'>
<div className='flex items-center justify-between'>
<div className='text-base font-semibold text-gray-900'></div>
<div className='flex items-center gap-2'>
<Button className='py-1 px-3' onClick={() => setShowDaojiandanSignature(false)} disabled={busy}>
</Button>
</div>
</div>
<div className='ui7-signature-wrapper border rounded-xl overflow-hidden bg-gray-50'>
<SignaturePad
ref={daojiandanSignaturePadRef}
className='ui7-signature-canvas w-full h-72 bg-white touch-none'
/>
<div className='flex items-center justify-between px-3 py-2 bg-gray-50 border-t'>
<div className='text-xs text-gray-500'></div>
<Button className='ui7-clear-button py-1 px-3' onClick={() => daojiandanSignaturePadRef.current?.clear()} disabled={daojiandanSubmitLoading}>
</Button>
</div>
</div>
{daojiandanSubmitMessage && (
<div className={`text-sm text-center ${daojiandanSubmitMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>{daojiandanSubmitMessage}</div>
)}
<div className='flex items-center justify-end gap-3'>
<Button
className='py-2 px-6'
onClick={() => {
setShowDaojiandanSignature(false);
setDaojiandanSubmitMessage(null);
daojiandanSignaturePadRef.current?.clear();
}}
disabled={daojiandanSubmitLoading}
>
</Button>
<Button
className='py-2 px-6 bg-blue-600 text-white hover:bg-blue-700'
onClick={handleSubmitDaojiandanSign}
disabled={daojiandanSubmitLoading}
>
{daojiandanSubmitLoading ? '提交中...' : '提交签名'}
</Button>
</div>
</div>
</div>
)
}
{
showDaojiandanPreview && daojiandanUrl && (
<div className='fixed inset-0 z-[60] bg-black/75 flex flex-col'>
<div className='flex items-center justify-between p-4 text-white bg-gray-900/80'>
<div className='text-sm font-medium truncate pr-3'></div>
<div className='flex items-center gap-2'>
<Button
className='py-1 px-3 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed'
onClick={handleDaojiandanDirectPrint}
disabled={busy}
>
</Button>
<Button
className='py-1 px-3 hover:bg-blue-700 text-white'
onClick={() => {
if (busy) return;
setShowDaojiandanPreview(false);
setShowDaojiandanSignature(true);
}}
disabled={busy}
>
</Button>
<Button className='py-1 px-3' onClick={() => !busy && setShowDaojiandanPreview(false)} disabled={busy}>
</Button>
</div>
</div>
<div className='flex-1 bg-gray-100 overflow-auto'>
{daojiandanPdfLoading ? (
<div className='flex items-center justify-center h-full text-white'>
<div>PDF...</div>
</div>
) : daojiandanPdfData ? (
<div className='flex justify-center p-4'>
<div ref={daojiandanCanvasContainerRef} className='w-full' />
</div>
) : (
<div className='flex items-center justify-center h-full text-white'>
<div>PDF加载失败</div>
</div>
)}
</div>
</div>
)
}
{
showAddItemBillSignature && (
<div className='fixed inset-0 z-[70] bg-black/80 flex items-center justify-center px-6'>
<div className='bg-white rounded-2xl w-full max-w-3xl shadow-2xl p-4 flex flex-col gap-4'>
<div className='flex items-center justify-between'>
<div className='text-base font-semibold text-gray-900'></div>
<div className='flex items-center gap-2'>
<Button className='py-1 px-3' onClick={() => setShowAddItemBillSignature(false)} disabled={busy}>
</Button>
</div>
</div>
<div className='ui7-signature-wrapper border rounded-xl overflow-hidden bg-gray-50'>
<SignaturePad
ref={addItemBillSignaturePadRef}
className='ui7-signature-canvas w-full h-72 bg-white touch-none'
/>
<div className='flex items-center justify-between px-3 py-2 bg-gray-50 border-t'>
<div className='text-xs text-gray-500'></div>
<Button className='ui7-clear-button py-1 px-3' onClick={() => addItemBillSignaturePadRef.current?.clear()} disabled={addItemBillSubmitLoading}>
</Button>
</div>
</div>
{addItemBillSubmitMessage && (
<div className={`text-sm text-center ${addItemBillSubmitMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>{addItemBillSubmitMessage}</div>
)}
<div className='flex items-center justify-end gap-3'>
<Button
className='py-2 px-6'
onClick={() => {
setShowAddItemBillSignature(false);
setAddItemBillSubmitMessage(null);
addItemBillSignaturePadRef.current?.clear();
}}
disabled={addItemBillSubmitLoading}
>
</Button>
<Button
className='py-2 px-6 bg-blue-600 text-white hover:bg-blue-700'
onClick={handleSubmitAddItemBillSign}
disabled={addItemBillSubmitLoading}
>
{addItemBillSubmitLoading ? '提交中...' : '提交签名'}
</Button>
</div>
</div>
</div>
)
}
{
showAddItemBillPreview && currentAddItemBill && currentAddItemBill.pdf_url && (
<div className='fixed inset-0 z-[60] bg-black/75 flex flex-col'>
<div className='flex items-center justify-between p-4 text-white bg-gray-900/80'>
<div className='text-sm font-medium truncate pr-3'>{currentAddItemBill.pdf_name || '加项单'}</div>
<div className='flex items-center gap-2'>
{currentAddItemBill.is_signed && (
<Button
className='py-1 px-3 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed'
onClick={() => handleAddItemBillDirectPrint(currentAddItemBill)}
disabled={busy}
>
</Button>
)}
<Button
className='py-1 px-3 hover:bg-blue-700 text-white'
onClick={() => {
if (busy) return;
setShowAddItemBillPreview(false);
setShowAddItemBillSignature(true);
}}
disabled={busy}
>
</Button>
<Button className='py-1 px-3' onClick={() => !busy && setShowAddItemBillPreview(false)} disabled={busy}>
</Button>
</div>
</div>
<div className='flex-1 bg-gray-100 overflow-auto'>
{addItemBillPdfLoading ? (
<div className='flex items-center justify-center h-full text-white'>
<div>PDF...</div>
</div>
) : addItemBillPdfData ? (
<div className='flex justify-center p-4'>
<div ref={addItemBillCanvasContainerRef} className='w-full' />
</div>
) : (
<div className='flex items-center justify-center h-full text-white'>
<div>PDF加载失败</div>
</div>
)}
</div>
</div>
)
}
</div >
);
};