保存加项PDF与导检单签名判断

This commit is contained in:
xianyi
2026-01-05 17:16:32 +08:00
parent b41deedd00
commit f8fdebb955
4 changed files with 227 additions and 46 deletions

View File

@@ -367,10 +367,11 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
}); });
if (res.Status === 200 && res.Data?.pdf_url) { if (res.Status === 200 && res.Data?.pdf_url) {
// 保存加项PDF信息到localStorage // 保存加项PDF信息到localStorage(未签名状态)
setAddItemBillPdf(examId, { setAddItemBillPdf(examId, {
pdf_name: res.Data.pdf_name || '加项单', pdf_name: res.Data.pdf_name || '加项单',
pdf_url: res.Data.pdf_url, pdf_url: res.Data.pdf_url,
is_signed: false,
}); });
return true; return true;
} else { } else {

View File

@@ -75,6 +75,7 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => {
setDaojiandanPdf(examId, { setDaojiandanPdf(examId, {
pdf_name: pdfNameValue, pdf_name: pdfNameValue,
pdf_url: pdfUrlValue, pdf_url: pdfUrlValue,
is_signed: true,
}); });
// 记录打印导检单是否签名操作 // 记录打印导检单是否签名操作
setExamActionRecord(examId, 'printSign', true); setExamActionRecord(examId, 'printSign', true);
@@ -134,13 +135,8 @@ export const ExamPrintPanel = ({ client }: { client: ExamClient }) => {
const res = await getDaojiandanPdfApi({ exam_id: examId }); const res = await getDaojiandanPdfApi({ exam_id: examId });
if (res.Status === 200 && res.Data?.pdf_url) { if (res.Status === 200 && res.Data?.pdf_url) {
const pdfUrlValue = res.Data.pdf_url; const pdfUrlValue = res.Data.pdf_url;
const pdfNameValue = res.Data.pdf_name || '导检单';
setPdfUrl(pdfUrlValue); setPdfUrl(pdfUrlValue);
// 保存到localStorage // 获取到的导检单是未签名的,不保存到localStorage,只用于显示
setDaojiandanPdf(examId, {
pdf_name: pdfNameValue,
pdf_url: pdfUrlValue,
});
setShowPreview(true); setShowPreview(true);
} else { } else {
setError(res.Message || '获取导检单失败'); setError(res.Message || '获取导检单失败');

View File

@@ -3,13 +3,14 @@ import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'; import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import type { OutputTongyishuFileInfo } from '../../api'; import type { OutputTongyishuFileInfo } from '../../api';
import { getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign, submitDaojiandanSign, editDaojiandanPrintStatus, getDaojiandanPdf as getDaojiandanPdfApi } from '../../api'; import { getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign, submitDaojiandanSign, editDaojiandanPrintStatus, getDaojiandanPdf as getDaojiandanPdfApi, getAddItemBillPdf as getAddItemBillPdfApi, submitAddItemBillSign } from '../../api';
import { import {
setExamActionRecord, setExamActionRecord,
setTongyishuPdfList, setTongyishuPdfList,
getTongyishuPdfList, getTongyishuPdfList,
setDaojiandanPdf, setDaojiandanPdf,
getDaojiandanPdf as getDaojiandanPdfFromStorage, getDaojiandanPdf as getDaojiandanPdfFromStorage,
setAddItemBillPdf,
getAddItemBillPdf as getAddItemBillPdfFromStorage, getAddItemBillPdf as getAddItemBillPdfFromStorage,
type TongyishuPdfInfo, type TongyishuPdfInfo,
} from '../../utils/examActions'; } from '../../utils/examActions';
@@ -70,9 +71,14 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
// 加项单相关状态 // 加项单相关状态
const [addItemBillUrl, setAddItemBillUrl] = useState<string | null>(null); const [addItemBillUrl, setAddItemBillUrl] = useState<string | null>(null);
const [addItemBillName, setAddItemBillName] = useState<string>('加项单'); const [addItemBillName, setAddItemBillName] = useState<string>('加项单');
const [isAddItemBillSigned, setIsAddItemBillSigned] = useState(false); // 加项单是否已签名
const [showAddItemBillPreview, setShowAddItemBillPreview] = useState(false); 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);
const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading; const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading || addItemBillSubmitLoading;
useEffect(() => { useEffect(() => {
onBusyChange?.(busy); onBusyChange?.(busy);
@@ -303,11 +309,12 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
useEffect(() => { useEffect(() => {
if (!examId) return; if (!examId) return;
// 先检查 localStorage 中的导检单(已签名的) // 先检查 localStorage 中的导检单
const storedPdf = getDaojiandanPdfFromStorage(examId); const storedPdf = getDaojiandanPdfFromStorage(examId);
if (storedPdf && storedPdf.pdf_url) { if (storedPdf && storedPdf.pdf_url) {
setDaojiandanUrl(storedPdf.pdf_url); setDaojiandanUrl(storedPdf.pdf_url);
setIsDaojiandanSigned(true); // localStorage 中的是已签名 // 使用 localStorage 中保存的 is_signed 字段判断是否已签名
setIsDaojiandanSigned(storedPdf.is_signed === true);
} else { } else {
// 如果 localStorage 中没有导检单,调用接口获取未签名的导检单用于查看和签名 // 如果 localStorage 中没有导检单,调用接口获取未签名的导检单用于查看和签名
const fetchDaojiandan = async () => { const fetchDaojiandan = async () => {
@@ -326,11 +333,31 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
fetchDaojiandan(); fetchDaojiandan();
} }
// 检查加项单PDF // 检查加项单PDF(逻辑和导检单一样)
const storedAddItemBill = getAddItemBillPdfFromStorage(examId); const storedAddItemBill = getAddItemBillPdfFromStorage(examId);
if (storedAddItemBill && storedAddItemBill.pdf_url) { if (storedAddItemBill && storedAddItemBill.pdf_url) {
setAddItemBillUrl(storedAddItemBill.pdf_url); setAddItemBillUrl(storedAddItemBill.pdf_url);
setAddItemBillName(storedAddItemBill.pdf_name || '加项单'); setAddItemBillName(storedAddItemBill.pdf_name || '加项单');
// 使用 localStorage 中保存的 is_signed 字段判断是否已签名
setIsAddItemBillSigned(storedAddItemBill.is_signed === true);
} else {
// 如果 localStorage 中没有加项单,调用接口获取未签名的加项单用于查看和签名
const fetchAddItemBill = async () => {
try {
const res = await getAddItemBillPdfApi({ exam_id: examId });
if (res.Status === 200 && res.Data?.pdf_url) {
const pdfUrlValue = res.Data.pdf_url;
const pdfNameValue = res.Data.pdf_name || '加项单';
// 不保存到 localStorage只用于显示和签名
setAddItemBillUrl(pdfUrlValue);
setAddItemBillName(pdfNameValue);
setIsAddItemBillSigned(false); // 接口获取的是未签名的
}
} catch (err) {
console.error('获取加项单失败', err);
}
};
fetchAddItemBill();
} }
}, [examId]); }, [examId]);
@@ -620,10 +647,11 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
setDaojiandanUrl(pdfUrlValue); setDaojiandanUrl(pdfUrlValue);
setIsDaojiandanSigned(true); // 签名成功后标记为已签名 setIsDaojiandanSigned(true); // 签名成功后标记为已签名
// 保存导检单PDF信息到localStorage // 保存导检单PDF信息到localStorage(标记为已签名)
setDaojiandanPdf(examId, { setDaojiandanPdf(examId, {
pdf_name: pdfNameValue, pdf_name: pdfNameValue,
pdf_url: pdfUrlValue, pdf_url: pdfUrlValue,
is_signed: true,
}); });
// 记录打印导检单是否签名操作 // 记录打印导检单是否签名操作
@@ -653,6 +681,61 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
} }
}; };
// 加项单签名提交
const handleSubmitAddItemBillSign = async () => {
if (!examId) {
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,
sign_file: blob,
});
if (res.Status === 200 && res.Data?.pdf_url) {
setAddItemBillSubmitMessage('签名提交成功');
const pdfUrlValue = res.Data.pdf_url;
const pdfNameValue = res.Data.pdf_name || '加项单';
setAddItemBillUrl(pdfUrlValue);
setAddItemBillName(pdfNameValue);
setIsAddItemBillSigned(true); // 签名成功后标记为已签名
// 保存加项单PDF信息到localStorage标记为已签名
setAddItemBillPdf(examId, {
pdf_name: pdfNameValue,
pdf_url: pdfUrlValue,
is_signed: true,
});
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 () => { const handleAddItemBillDirectPrint = async () => {
if (!addItemBillUrl) return; if (!addItemBillUrl) return;
@@ -1265,34 +1348,65 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
<div className='flex items-center justify-between gap-3 p-2 rounded-xl border bg-white shadow-sm'> <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'> <div className='text-sm text-gray-800 truncate flex items-center gap-2 relative pr-20'>
<span className='truncate'>{addItemBillName.length > 12 ? addItemBillName.slice(0, 12) + "..." : addItemBillName}</span> <span className='truncate'>{addItemBillName.length > 12 ? addItemBillName.slice(0, 12) + "..." : addItemBillName}</span>
<img {isAddItemBillSigned && (
src='/sign.png' <img
alt='已生成' src='/sign.png'
className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none' alt='已签名'
loading='lazy' className='w-16 h-16 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none select-none'
/> loading='lazy'
/>
)}
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Button {isAddItemBillSigned ? (
className='py-1.5 px-3 bg-blue-600 hover:bg-blue-700 text-white' // 已签名:显示打印和查看按钮
onClick={() => { <>
if (busy) return; <Button
handleAddItemBillDirectPrint(); className='py-1.5 px-3 bg-blue-600 hover:bg-blue-700 text-white'
}} onClick={() => {
disabled={busy} if (busy) return;
> handleAddItemBillDirectPrint();
}}
</Button> disabled={busy}
<Button >
className='py-1.5 px-3'
onClick={() => { </Button>
if (busy) return; <Button
setShowAddItemBillPreview(true); className='py-1.5 px-3'
}} onClick={() => {
disabled={busy} if (busy) return;
> setShowAddItemBillPreview(true);
}}
</Button> disabled={busy}
>
</Button>
</>
) : (
// 未签名:显示查看和签名按钮
<>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setShowAddItemBillPreview(true);
}}
disabled={busy}
>
</Button>
<Button
className='py-1.5 px-3'
onClick={() => {
if (busy) return;
setShowAddItemBillSignature(true);
}}
disabled={busy}
>
</Button>
</>
)}
</div> </div>
</div> </div>
)} )}
@@ -1471,19 +1585,85 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
</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 && addItemBillUrl && ( showAddItemBillPreview && addItemBillUrl && (
<div className='fixed inset-0 z-[60] bg-black/75 flex flex-col'> <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='flex items-center justify-between p-4 text-white bg-gray-900/80'>
<div className='text-sm font-medium truncate pr-3'>{addItemBillName}</div> <div className='text-sm font-medium truncate pr-3'>{addItemBillName}</div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Button {isAddItemBillSigned && (
className='py-1 px-3 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed' <Button
onClick={handleAddItemBillDirectPrint} className='py-1 px-3 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed'
disabled={busy} onClick={handleAddItemBillDirectPrint}
> disabled={busy}
>
</Button>
</Button>
)}
{!isAddItemBillSigned && (
<Button
className='py-1 px-3 bg-blue-600 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 className='py-1 px-3' onClick={() => !busy && setShowAddItemBillPreview(false)} disabled={busy}>
</Button> </Button>

View File

@@ -146,6 +146,8 @@ export interface DaojiandanPdfInfo {
pdf_name: string; pdf_name: string;
/** PDF文件地址 */ /** PDF文件地址 */
pdf_url: string; pdf_url: string;
/** 是否已签名 */
is_signed?: boolean;
} }
/** /**
@@ -196,6 +198,8 @@ export interface AddItemBillPdfInfo {
pdf_name: string; pdf_name: string;
/** PDF文件地址 */ /** PDF文件地址 */
pdf_url: string; pdf_url: string;
/** 是否已签名 */
is_signed?: boolean;
} }
/** /**