2151 lines
74 KiB
TypeScript
2151 lines
74 KiB
TypeScript
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 } 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 {
|
||
setDaojiandanUrl(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;
|
||
}
|
||
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);
|
||
}
|
||
}, 2000);
|
||
} 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);
|
||
}, 2000);
|
||
} 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;
|
||
}
|
||
|
||
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 unsigned = addItemList.find((bill) => bill.is_signed !== true);
|
||
setCurrentAddItemBill(unsigned || addItemList[0] || null);
|
||
} else {
|
||
setAddItemBillList([]);
|
||
setCurrentAddItemBill(null);
|
||
}
|
||
} catch (e) {
|
||
console.error('刷新加项单列表失败', e);
|
||
}
|
||
}
|
||
|
||
setTimeout(() => {
|
||
setShowAddItemBillSignature(false);
|
||
setAddItemBillSubmitMessage(null);
|
||
addItemBillSignaturePadRef.current?.clear();
|
||
setShowAddItemBillPreview(true);
|
||
}, 2000);
|
||
} 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'>
|
||
{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 >
|
||
);
|
||
};
|
||
|
||
|