Files
ipad/src/components/exam/ExamSignPanel.tsx
2026-01-16 10:29:39 +08:00

2575 lines
94 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, OutputPhysicalExamItemInfo } from '../../api';
import {
signInMedicalExamCenter,
submitTongyishuSign,
submitDaojiandanSign,
editDaojiandanPrintStatus,
submitAddItemBillSign,
getTijianPdfFile,
getTongyishuPdf,
getDaojiandanPdf,
getExamOptionalItemList,
removeOptionalPackage,
getExamOptionRecordList,
} 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 [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 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 [batchPrintLoading, setBatchPrintLoading] = useState(false);
const addItemBillCanvasContainerRef = useRef<HTMLDivElement>(null);
// 可选项目列表相关状态
const [optionalItemList, setOptionalItemList] = useState<OutputPhysicalExamItemInfo[]>([]);
const [optionalItemLoading, setOptionalItemLoading] = useState(false);
const [selectedOptionalItem, setSelectedOptionalItem] = useState<number | null>(null);
// 是否已经确认过可选项目(如果后端已有记录,视为已确认)
const [optionalConfirmed, setOptionalConfirmed] = useState(false);
// 是否显示"请先确认体检项目,确认后不可修改"的提示
const [showOptionalConfirmTip, setShowOptionalConfirmTip] = useState(false);
// 跟踪当前 examId 是否已加载过导检单和知情同意书
const pdfsLoadedForExamIdRef = useRef<number | null>(null);
// 使用 ref 存储最新的可选项目状态,确保 refreshTijianPdfs 能读取到最新值
const optionalItemListRef = useRef<OutputPhysicalExamItemInfo[]>([]);
const optionalConfirmedRef = useRef<boolean>(false);
const busy =
signLoading ||
submitLoading ||
consentLoading ||
pdfLoading ||
daojiandanSubmitLoading ||
addItemBillSubmitLoading ||
batchPrintLoading ||
optionalItemLoading;
useEffect(() => {
onBusyChange?.(busy);
return () => onBusyChange?.(false);
}, [busy, onBusyChange]);
const refreshTijianPdfs = async (examIdValue: number, checkOptional: boolean = true) => {
// 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值)
if (checkOptional && optionalItemListRef.current.length > 0 && !optionalConfirmedRef.current) {
console.log('跳过 pdf-file-get 请求:可选项目未确认', { optionalItemListLength: optionalItemListRef.current.length, optionalConfirmed: optionalConfirmedRef.current });
return;
}
setConsentLoading(true);
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);
// 导检单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 / combination_code / is_pay / is_pay_name
pdf_sort: item.pdf_sort ?? 0,
combinationCode: item.combination_code != null ? 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);
} finally {
setConsentLoading(false);
}
};
// 加载可选项目列表 & 操作记录(优先使用记录,无记录时才调用可选项目接口)
const loadOptionalItems = async () => {
if (!examId) return;
setOptionalItemLoading(true);
try {
// 先获取操作记录
const recordRes = await getExamOptionRecordList({ exam_id: examId });
console.log("操作记录请求", recordRes);
// 若任意记录 is_abandon=1则视为"已确定"(已做过弃选/确认动作),不可再确认
let hasConfirmedRecord = false;
let selectedFromRecord: number | null = null;
let itemsFromRecord: OutputPhysicalExamItemInfo[] = [];
if (recordRes.Status === 200 && recordRes.Data && recordRes.Data.length > 0) {
hasConfirmedRecord = recordRes.Data.some((r) => r.is_abandon === 1);
// 从记录中构建可选项目列表(只显示 is_abandon=0 的记录)
itemsFromRecord = recordRes.Data
.filter((r) => r.is_abandon === 0 && r.combination_code)
.map((r) => {
const codeNum = Number(r.combination_code);
const packageNum = r.package_code ? Number(r.package_code) : null;
if (!Number.isFinite(codeNum)) return null;
return {
combination_code: codeNum,
combination_name: r.combination_name || '',
package_code: Number.isFinite(packageNum) ? packageNum : null,
} as OutputPhysicalExamItemInfo;
})
.filter((item): item is OutputPhysicalExamItemInfo => item !== null);
// 预选:优先取 is_abandon=0 的组合码(若有)
const selectedRecord = recordRes.Data.find(
(r) => r.is_abandon === 0 && r.combination_code
);
if (selectedRecord?.combination_code) {
const codeNum = Number(selectedRecord.combination_code);
if (Number.isFinite(codeNum)) {
selectedFromRecord = codeNum;
}
}
}
// 如果有记录,使用记录构建的列表;否则调用可选项目接口
if (itemsFromRecord.length > 0) {
// 根据 combination_code 去重,保留第一个出现的项目
const uniqueItems = itemsFromRecord.filter((item, index, self) =>
index === self.findIndex((t) => t.combination_code === item.combination_code)
);
setOptionalItemList(uniqueItems);
optionalItemListRef.current = uniqueItems;
if (selectedFromRecord != null && uniqueItems.some((i) => i.combination_code === selectedFromRecord)) {
setSelectedOptionalItem(selectedFromRecord);
} else {
setSelectedOptionalItem(null);
}
// 只要存在可选项目:默认未确认;但若出现 is_abandon=1 记录,则视为已确定
setOptionalConfirmed(hasConfirmedRecord);
optionalConfirmedRef.current = hasConfirmedRecord;
setShowOptionalConfirmTip(false);
} else {
// 没有记录,调用可选项目接口
const listRes = await getExamOptionalItemList({ physical_exam_id: examId });
if (listRes.Status === 200 && listRes.Data?.listOptionalItem) {
const items = listRes.Data.listOptionalItem;
// 根据 combination_code 去重,保留第一个出现的项目
const uniqueItems = items.filter((item, index, self) =>
index === self.findIndex((t) => t.combination_code === item.combination_code)
);
setOptionalItemList(uniqueItems);
optionalItemListRef.current = uniqueItems;
// 无历史记录,默认不选
setSelectedOptionalItem(null);
// 只要存在可选项目:默认未确认
setOptionalConfirmed(false);
optionalConfirmedRef.current = false;
setShowOptionalConfirmTip(false);
} else {
setOptionalItemList([]);
optionalItemListRef.current = [];
setSelectedOptionalItem(null);
setOptionalConfirmed(true);
optionalConfirmedRef.current = true;
setShowOptionalConfirmTip(false);
}
}
} catch (err) {
console.error('获取可选项目/记录失败', err);
setOptionalItemList([]);
optionalItemListRef.current = [];
setSelectedOptionalItem(null);
setOptionalConfirmed(true);
optionalConfirmedRef.current = true;
setShowOptionalConfirmTip(false);
} finally {
setOptionalItemLoading(false);
}
};
// 加载 PDF 的逻辑(知情同意书、导检单)
const loadPdfs = async (currentExamId: number) => {
// 如果已经为当前 examId 加载过,不再重复加载
if (pdfsLoadedForExamIdRef.current === currentExamId) {
return;
}
// 使用 ref 确保读取最新值:如果没有可选项目,或者有可选项目但已确认,才加载导检单和知情同意书
console.log('检查是否应该加载 PDFref 值:', {
optionalItemListLength: optionalItemListRef.current.length,
optionalConfirmed: optionalConfirmedRef.current
});
const shouldLoadPdfs = optionalItemListRef.current.length === 0 || optionalConfirmedRef.current;
if (!shouldLoadPdfs) {
console.log('跳过所有 PDF 接口请求:可选项目未确认', {
optionalItemListLength: optionalItemListRef.current.length,
optionalConfirmed: optionalConfirmedRef.current
});
return;
}
pdfsLoadedForExamIdRef.current = currentExamId;
// 初始化时加载知情同意书
const loadTongyishu = async () => {
try {
const res = await getTongyishuPdf({ exam_id: currentExamId });
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);
}
} catch (err) {
console.error('获取知情同意书失败', err);
}
};
// 初始化时加载导检单(未签名版本也可以查看)
const loadDaojiandan = async () => {
try {
console.log("未签名导检单请求");
const res = await getDaojiandanPdf({ exam_id: currentExamId });
if (res.Status === 200 && res.Data?.pdf_url) {
setDaojiandanUrl((prev) => prev || res.Data?.pdf_url || null);
}
} catch (err) {
console.error('获取导检单失败', err);
}
};
// 加载 PDF
loadTongyishu().then(() => {
// 调用 refreshTijianPdfs 前再次检查可选项目状态(使用 ref 确保读取最新值)
if (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current) {
refreshTijianPdfs(currentExamId, true);
}
});
loadDaojiandan();
};
// 初始化:先检查可选项
useEffect(() => {
if (!examId) {
setConsentList([]);
setDaojiandanUrl(null);
setIsDaojiandanSigned(false);
optionalItemListRef.current = [];
optionalConfirmedRef.current = true;
pdfsLoadedForExamIdRef.current = null;
setOptionalItemLoading(false);
return;
}
// 先设置 loading 为 true
setOptionalItemLoading(true);
optionalItemListRef.current = [];
optionalConfirmedRef.current = false;
pdfsLoadedForExamIdRef.current = null;
// 先检查可选项,选择完毕后才请求导检单接口
loadOptionalItems().then(() => {
// loadOptionalItems 完成后ref 已经更新,直接调用加载 PDF 的逻辑
console.log('loadOptionalItems 完成ref 已更新', {
optionalItemListLength: optionalItemListRef.current.length,
optionalConfirmed: optionalConfirmedRef.current
});
// 直接调用加载 PDF 的逻辑,不依赖第二个 useEffect
loadPdfs(examId);
});
}, [examId]);
// 监听可选项目确认状态变化,确认后才加载导检单和知情同意书
useEffect(() => {
if (!examId) return;
// 仅在可选项目状态从"未确认"变为"已确认"时,加载 PDF
if (optionalConfirmed && optionalItemList.length > 0 && pdfsLoadedForExamIdRef.current !== examId) {
console.log('可选项目已确认,开始加载 PDF');
loadPdfs(examId);
}
}, [examId, optionalConfirmed]);
const handlePickFile = () => {
// 有可选项目但尚未确认时,禁止拍照并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
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;
}
if (!examId) {
setMessage('缺少体检ID');
return;
}
// 如果存在可选项目但尚未确认,则不允许直接签到
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
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 handleConfirmOptionalSelection = async () => {
if (!examId) {
setMessage('缺少体检ID');
return;
}
if (optionalItemList.length === 0) {
setOptionalConfirmed(true);
optionalConfirmedRef.current = true;
setShowOptionalConfirmTip(false);
return;
}
if (selectedOptionalItem == null) {
setMessage('请选择一个可选项目');
setShowOptionalConfirmTip(true);
return;
}
const unselectedCodes = optionalItemList
.map((item) => item.combination_code)
.filter((code): code is number => code != null && code !== selectedOptionalItem);
// 没有未选项目需要移除,直接标记为已确认
if (unselectedCodes.length === 0) {
setOptionalConfirmed(true);
optionalConfirmedRef.current = true;
setShowOptionalConfirmTip(false);
setMessage('可选项目已确定');
setTimeout(() => setMessage(null), 3000);
return;
}
setOptionalItemLoading(true);
try {
const combinationCodeIds = unselectedCodes.join(',');
const res = await removeOptionalPackage({
physical_exam_id: examId,
combination_code_ids: combinationCodeIds,
});
if (res.Status === 200 && res.Data?.is_success === 1) {
setOptionalConfirmed(true);
optionalConfirmedRef.current = true;
setShowOptionalConfirmTip(false);
// 刷新操作记录列表(不再调用 optional-item-list 接口,避免恢复已移除的项目)
await loadOptionalItems();
setMessage('可选项目已确定');
setTimeout(() => setMessage(null), 3000);
} else {
setMessage('可选项目确定失败:' + (res.Message || '未知错误'));
setTimeout(() => setMessage(null), 3000);
}
} catch (err) {
console.error('可选项目确定失败', err);
setMessage('可选项目确定失败,请稍后重试');
setTimeout(() => setMessage(null), 3000);
} finally {
setOptionalItemLoading(false);
}
};
const handleSubmitSign = async () => {
// 有可选项目但尚未确认时,禁止签名并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setSubmitMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
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 列表)
// 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值)
if (examId && (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current)) {
refreshTijianPdfs(examId, true);
} else if (examId) {
console.log('跳过 pdf-file-get 请求:可选项目未确认(签名成功后)', {
optionalItemListLength: optionalItemListRef.current.length,
optionalConfirmed: optionalConfirmedRef.current
});
}
}, 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);
return;
}
let objectUrl: string | null = null;
setDaojiandanPdfLoading(true);
setDaojiandanPdfData(null);
fetch(daojiandanUrl)
.then((resp) => {
if (!resp.ok) throw new Error('获取PDF文件失败');
return resp.blob();
})
.then((blob) => {
objectUrl = URL.createObjectURL(blob);
return blob.arrayBuffer();
})
.then((arrayBuffer) => {
setDaojiandanPdfData(arrayBuffer);
setDaojiandanPdfLoading(false);
})
.catch((err) => {
console.error('导检单PDF 拉取失败', err);
});
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [showDaojiandanPreview, daojiandanUrl]);
// 渲染导检单 PDF
useEffect(() => {
if (!daojiandanPdfData || !daojiandanCanvasContainerRef.current) return;
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;
}
} catch (err) {
console.error('导检单PDF 渲染失败', err);
}
};
renderAllPages();
}, [daojiandanPdfData]);
// 加载加项单 PDF 数据(当前选中的加项单)
useEffect(() => {
if (!showAddItemBillPreview || !currentAddItemBill?.pdf_url) {
setAddItemBillPdfData(null);
setAddItemBillPdfLoading(false);
return;
}
let objectUrl: string | null = null;
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);
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]);
// 导检单预览:使用 pdfjs 渲染到 canvas
useEffect(() => {
if (!showDaojiandanPreview || !daojiandanUrl || !daojiandanCanvasContainerRef.current) return;
const container = daojiandanCanvasContainerRef.current;
let cancelled = false;
const renderDaojiandan = async () => {
try {
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) {
}
}
};
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(); setTimeout(() => window.close(), 1000);">打印</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(); setTimeout(() => window.close(), 1000);">打印</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 (optionalItemList.length > 0 && !optionalConfirmed) {
setDaojiandanSubmitMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
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;
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 列表,获取最新的加项单签名状态和地址
// 如果还有可选项目未确认,不执行接口请求(使用 ref 确保读取最新值)
if (examId && (optionalItemListRef.current.length === 0 || optionalConfirmedRef.current)) {
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) => ({
// 同样直接使用接口返回的字段,保持与 refreshTijianPdfs 一致
pdf_sort: item.pdf_sort ?? 0,
combinationCode: item.combination_code != null ? 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(); setTimeout(() => window.close(), 1000);">打印</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(); setTimeout(() => window.close(), 1000);">打印</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 || batchPrintLoading) return;
setBatchPrintLoading(true);
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(); setTimeout(() => window.close(), 1000);">打印</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('一键打印失败,请稍后重试');
} finally {
setBatchPrintLoading(false);
}
};
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 || (optionalItemList.length > 0 && !optionalConfirmed)}
>
{signLoading ? '签到中...' : '签到'}
</Button>
</div>
{message && (
<div className={`text-xs ${message.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>{message}</div>
)}
{/* 可选项目列表 */}
{optionalItemLoading ? (
<div className='text-xs text-gray-500'>...</div>
) : optionalItemList.length > 0 ? (
<div className='mt-2 space-y-2'>
<div className='text-xs font-medium text-gray-700'>
{optionalConfirmed ? '(已确定)' : ''}
</div>
<div className='max-h-32 overflow-y-auto custom-scroll space-y-1'>
{optionalItemList.map((item) => {
const combinationCode = item.combination_code;
const isSelected =
combinationCode != null && combinationCode === selectedOptionalItem;
return (
<label
key={combinationCode ?? `item-${item.package_code}`}
className='flex items-center gap-2 cursor-pointer text-xs'
>
<input
type='radio'
name='optional-item'
checked={isSelected}
onChange={() => {
if (combinationCode != null && !optionalConfirmed) {
setSelectedOptionalItem(combinationCode);
}
}}
disabled={busy || optionalConfirmed}
className='w-3 h-3'
/>
<span className={isSelected ? 'text-blue-600 font-medium' : 'text-gray-700'}>
{item.combination_name || '未命名项目'}
</span>
</label>
);
})}
</div>
{!optionalConfirmed && (
<div className='flex items-center gap-2'>
<Button
className='py-1 px-3 text-xs bg-blue-600 hover:bg-blue-700 text-white'
onClick={handleConfirmOptionalSelection}
disabled={busy || selectedOptionalItem == null}
>
</Button>
{showOptionalConfirmTip && (
<span className='text-[11px] text-gray-500'>
</span>
)}
</div>
)}
</div>
) : null}
</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={() => {
// 有可选项目但尚未确认时,禁止打印并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
handleBatchPrint();
}}
disabled={busy || batchPrintLoading}
>
{batchPrintLoading ? '加载中...' : '一键打印'}
</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 > 10 ? item.pdf_name.slice(0, 10) + "..." : 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;
// 有可选项目但尚未确认时,禁止打印并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
handleDirectPrint(item);
}}
disabled={busy}
>
</Button>
)}
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止查看并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
setPreviewPdf(item);
}}
disabled={busy}
>
</Button>
</div>
</div>
))}
{/* 只有在没有可选项目,或者有可选项目但已确认时,才显示导检单 */}
{(optionalItemList.length === 0 || optionalConfirmed) && (
<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;
// 有可选项目但尚未确认时,禁止打印并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
handleDaojiandanDirectPrint();
}}
disabled={busy}
>
</Button>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止查看并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
setShowDaojiandanPreview(true);
}}
disabled={busy}
>
</Button>
</>
) : daojiandanUrl ? (
// 未签名但有导检单:显示查看和签名按钮
<>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止查看并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
setShowDaojiandanPreview(true);
}}
disabled={busy}
>
</Button>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止签名并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
setShowDaojiandanSignature(true);
}}
disabled={busy}
>
</Button>
</>
) : (
// 没有导检单:只显示签名按钮
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止签名并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
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' ? `` : ''}
</span>
{bill.payment_status_name && (
<span className='text-xs text-gray-500 whitespace-nowrap'>{bill.payment_status_name}</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={() => {
// 有可选项目但尚未确认时,禁止打印并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
handlePrint();
}}
disabled={pdfLoading || !pdfReady || !pdfBlobUrl}
>
</Button>
<Button
className='py-1 px-3'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止签名并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
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={() => {
// 有可选项目但尚未确认时,禁止打印并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
return;
}
handleDaojiandanDirectPrint();
}}
disabled={busy}
>
</Button>
<Button
className='py-1 px-3 hover:bg-blue-700 text-white'
onClick={() => {
if (busy) return;
// 有可选项目但尚未确认时,禁止签名并提示先确认项目
if (optionalItemList.length > 0 && !optionalConfirmed) {
setMessage('请先确认体检项目');
setShowOptionalConfirmTip(true);
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 >
);
};