From f6cc55582eea1755e6856721585948f51569c619 Mon Sep 17 00:00:00 2001 From: xianyi Date: Sun, 4 Jan 2026 15:54:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=8A=A0=E9=A1=B9=E6=94=AF?= =?UTF-8?q?=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/his.ts | 36 ++- src/api/types.ts | 53 +++- src/components/exam/ExamAddonPanel.tsx | 350 ++++++++++++++++++++++++- src/components/exam/ExamSignPanel.tsx | 172 ++++++++++++ src/utils/examActions.ts | 72 ++++- 5 files changed, 642 insertions(+), 41 deletions(-) diff --git a/src/api/his.ts b/src/api/his.ts index 3d8511a..14408d4 100644 --- a/src/api/his.ts +++ b/src/api/his.ts @@ -22,6 +22,8 @@ import type { DaojiandanGetResponse, InputDaojiandanSignSubmit, DaojiandanSignSubmitResponse, + InputAddItemBillSignSubmit, + AddItemBillSignSubmitResponse, InputDaojiandanPrintStatus, DaojiandanPrintStatusResponse, InputCustomerDetailEdit, @@ -156,7 +158,7 @@ export const signInMedicalExamCenter = ( ): Promise => { const formData = new FormData(); formData.append('id_no_pic', data.id_no_pic); - + return request.post( `${MEDICAL_EXAM_BASE_PATH}/sign-in`, formData, @@ -188,7 +190,7 @@ export const submitTongyishuSign = ( ): Promise => { const formData = new FormData(); formData.append('sign_file', data.sign_file); - + return request.post( `${MEDICAL_EXAM_BASE_PATH}/tongyishu-sign-submit?exam_id=${data.exam_id}&combination_code=${data.combination_code}`, formData, @@ -292,7 +294,7 @@ export const submitDaojiandanSign = ( ): Promise => { const formData = new FormData(); formData.append('sign_file', data.sign_file); - + return request.post( `${MEDICAL_EXAM_BASE_PATH}/daojiandan-sign-submit?exam_id=${data.exam_id}`, formData, @@ -304,6 +306,26 @@ export const submitDaojiandanSign = ( ).then(res => res.data); }; +/** + * 提交体检加项单签名生成PDF + */ +export const submitAddItemBillSign = ( + data: InputAddItemBillSignSubmit +): Promise => { + const formData = new FormData(); + formData.append('sign_file', data.sign_file); + + return request.post( + `${MEDICAL_EXAM_BASE_PATH}/add-item-bill-sign-submit?exam_id=${data.exam_id}`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ).then(res => res.data); +}; + /** * 体检导检单打印状态修改 */ @@ -408,15 +430,15 @@ export const getUserOwnedMenus = ( data: InputUserOwnedMenus ): Promise => { // 从 localStorage 获取 accessToken - const accessToken = typeof window !== 'undefined' - ? localStorage.getItem('accessToken') + const accessToken = typeof window !== 'undefined' + ? localStorage.getItem('accessToken') : null; - + const headers: Record = {}; if (accessToken) { headers.Authorization = `Bearer ${accessToken}`; } - + return request.post( `/api/auth/user/owned/menus`, data, diff --git a/src/api/types.ts b/src/api/types.ts index e3f6166..94df6de 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -400,6 +400,33 @@ export interface OutputDaojiandanSignInfo { */ export type DaojiandanSignSubmitResponse = CommonActionResult; +/** + * 提交体检加项单签名入参 + */ +export interface InputAddItemBillSignSubmit { + /** 体检ID */ + exam_id: number; + /** 签名图片文件 */ + sign_file: File | Blob; +} + +/** + * 提交体检加项单签名出参 + */ +export interface OutputAddItemBillSignInfo { + /** 加项单文件名称 */ + pdf_name?: string | null; + /** PDF文件地址 */ + pdf_url?: string | null; + /** 消息内容 */ + message?: string | null; +} + +/** + * 提交体检加项单签名响应 + */ +export type AddItemBillSignSubmitResponse = CommonActionResult; + /** * 体检导检单打印状态修改入参 */ @@ -457,6 +484,8 @@ export interface InputPhysicalExamAddItem { * 加项界面客户基本信息 */ export interface PoAddItemCustomerInfo { + /** 患者ID */ + patient_id: number; /** 客户姓名 */ customer_name?: string | null; /** 体检编号 */ @@ -595,16 +624,10 @@ export interface InputPhysicalExamAddOrder { userName: string; /** 用户手机号 */ userPhone: string; - /** 体检加项产品ID列表(逗号分隔) */ - addItemIds: string; - /** 微信Native产品ID */ - nativeProductId: string; - /** 订单金额(元) */ + /** 体检组合项目代码(addItemList.combination_item_code 多个加项逗号分隔) */ + combinationItemCodes: string; + /** 订单总金额(元) */ orderAmount: number; - /** 订单来源(mini_program-小程序,app-app应用等) */ - source: string; - /** 商户ID(可选,不传则使用默认商户) */ - merchantId?: string | null; } /** @@ -618,8 +641,16 @@ export type PhysicalExamQrcodeCreateResponse = CommonActionResult; export interface InputOrderPaymentInfo { /** 体检ID */ physical_exam_id: number; - /** 订单编号 */ - order_id: string; + /** 患者ID */ + patient_id: number; + /** 体检组合项目代码(addItemList.combination_item_code 多个加项逗号分隔) */ + combinationItemCodes: string; + /** 折扣比例 */ + discount_rate: number; + /** 支付方式(12-微信 13-挂账公司) */ + pay_type: number; + /** 挂账公司ID(挂账公司传对应的ID,其他传0) */ + company_id: number; } /** diff --git a/src/components/exam/ExamAddonPanel.tsx b/src/components/exam/ExamAddonPanel.tsx index 147fd04..96250a7 100644 --- a/src/components/exam/ExamAddonPanel.tsx +++ b/src/components/exam/ExamAddonPanel.tsx @@ -1,10 +1,11 @@ import { useEffect, useMemo, useState, useRef } from 'react'; import type { ExamClient } from '../../data/mockData'; -import { searchPhysicalExamAddItem, getAddItemCustomerInfo, getChannelCompanyList } from '../../api'; +import { searchPhysicalExamAddItem, getAddItemCustomerInfo, getChannelCompanyList, createNativePaymentQrcode, checkNativePaymentStatus, submitAddItemBillSign } from '../../api'; import { Button, Input } from '../ui'; import { cls } from '../../utils/cls'; import nozImage from '../../assets/image/noz.png'; +import { setAddItemBillPdf } from '../../utils/examActions'; interface AddonTag { title: string; @@ -18,6 +19,7 @@ interface AddonItem { tags?: AddonTag[]; originalPrice?: string; currentPrice?: string; + combinationItemCode?: number | null; } interface ExamAddonPanelProps { @@ -54,6 +56,18 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => { const [accountCompanyList, setAccountCompanyList] = useState< Array<{ company_id: number; company_name?: string | null; pinyin?: string | null }> >([]); + // 客户信息 + const [customerInfo, setCustomerInfo] = useState<{ + customer_name?: string | null; + phone?: string | null; + patient_id?: number | null; + } | null>(null); + // 支付相关状态 + const [showQrcodeModal, setShowQrcodeModal] = useState(false); + const [qrcodeUrl, setQrcodeUrl] = useState(null); + const [paymentLoading, setPaymentLoading] = useState(false); + const [paymentMessage, setPaymentMessage] = useState(null); + const pollingTimerRef = useRef(null); // 点击外部关闭下拉框 useEffect(() => { @@ -86,11 +100,24 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => { const fetchCustomerInfo = async () => { try { const res = await getAddItemCustomerInfo({ physical_exam_id }); - if (res.Status === 200 && res.Data?.listChannelDiscount && res.Data.listChannelDiscount.length > 0) { - setChannelDiscounts(res.Data.listChannelDiscount); - const rate = res.Data.listChannelDiscount[0]?.discount_rate; - if (typeof rate === 'number' && rate > 0) { - setDiscountRatio(rate); + if (res.Status === 200) { + // 保存客户信息 + if (res.Data?.customerInfo) { + setCustomerInfo({ + patient_id: res.Data.customerInfo.patient_id, + customer_name: res.Data.customerInfo.customer_name, + phone: res.Data.customerInfo.phone, + }); + } + // 保存渠道折扣信息 + if (res.Data?.listChannelDiscount && res.Data.listChannelDiscount.length > 0) { + setChannelDiscounts(res.Data.listChannelDiscount); + const rate = res.Data.listChannelDiscount[0]?.discount_rate; + if (typeof rate === 'number' && rate > 0) { + setDiscountRatio(rate); + } + } else { + setChannelDiscounts([]); } } else { setChannelDiscounts([]); @@ -182,6 +209,7 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => { : item.original_price !== undefined && item.original_price !== null ? Number(item.original_price).toFixed(2) : '0.00', + combinationItemCode: item.combination_item_code ?? null, tags: [], paid: false, })); @@ -318,6 +346,262 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => { })); }, [accountCompanyList]); + // 清理轮询定时器 + useEffect(() => { + return () => { + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + pollingTimerRef.current = null; + } + }; + }, []); + + // 创建空白签名图片(用于自动生成PDF) + const createBlankSignature = (): Promise => { + return new Promise((resolve) => { + const canvas = document.createElement('canvas'); + canvas.width = 400; + canvas.height = 200; + const ctx = canvas.getContext('2d'); + if (ctx) { + // 创建白色背景 + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + // 如果转换失败,创建一个空的 Blob + resolve(new Blob([], { type: 'image/png' })); + } + }, 'image/png'); + }); + }; + + // 获取加项PDF + const fetchAddItemBillPdf = async (examId: number) => { + try { + // 创建空白签名 + const blankSignature = await createBlankSignature(); + + // 调用接口获取加项PDF + const res = await submitAddItemBillSign({ + exam_id: examId, + sign_file: blankSignature, + }); + + if (res.Status === 200 && res.Data?.pdf_url) { + // 保存加项PDF信息 + setAddItemBillPdf(examId, { + pdf_name: res.Data.pdf_name || '加项单', + pdf_url: res.Data.pdf_url, + }); + return true; + } else { + console.error('获取加项PDF失败', res.Message); + return false; + } + } catch (err) { + console.error('获取加项PDF失败', err); + return false; + } + }; + + // 轮询查询支付结果 + const startPaymentPolling = ( + physical_exam_id: number, + patient_id: number, + combinationItemCodes: string, + discount_rate: number, + pay_type: number, + company_id: number + ) => { + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + } + + pollingTimerRef.current = window.setInterval(async () => { + try { + const res = await checkNativePaymentStatus({ + physical_exam_id, + patient_id, + combinationItemCodes, + discount_rate, + pay_type, + company_id, + }); + + if (res.Status === 200) { + const result = res.Data; + // 假设支付成功返回 "success" 或 "1",失败返回其他值 + if (result === 'success' || result === '1' || result === 'SUCCESS') { + // 支付成功 + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + pollingTimerRef.current = null; + } + setPaymentMessage('支付成功,正在生成加项单...'); + setShowQrcodeModal(false); + setQrcodeUrl(null); + // 清空已选项目 + setSelectedIds(new Set()); + // 获取加项PDF + fetchAddItemBillPdf(physical_exam_id).then((success) => { + if (success) { + setPaymentMessage('支付成功,加项单已生成'); + } else { + setPaymentMessage('支付成功,但加项单生成失败'); + } + setTimeout(() => { + setPaymentMessage(null); + }, 3000); + }); + } else if (result === 'failed' || result === '0' || result === 'FAILED') { + // 支付失败 + if (pollingTimerRef.current) { + clearInterval(pollingTimerRef.current); + pollingTimerRef.current = null; + } + setPaymentMessage('支付失败'); + setTimeout(() => { + setPaymentMessage(null); + }, 3000); + } + // 其他状态继续轮询 + } + } catch (err) { + console.error('查询支付状态失败', err); + } + }, 2000); // 每2秒轮询一次 + }; + + // 处理支付 + const handlePayment = async () => { + if (selectedCount === 0) return; + + const physical_exam_id = Number(client.id); + if (!physical_exam_id) { + setPaymentMessage('缺少体检ID'); + return; + } + + if (!customerInfo?.customer_name || !customerInfo?.phone) { + setPaymentMessage('缺少客户信息,请稍后重试'); + return; + } + + setPaymentLoading(true); + setPaymentMessage(null); + + try { + const selectedItems = allAddons.filter((item) => selectedIds.has(item.id || item.name)); + + // 获取组合项目代码(combination_item_code) + const combinationItemCodes = selectedItems + .map((item) => item.combinationItemCode) + .filter((code) => code !== null && code !== undefined) + .join(','); + + if (!combinationItemCodes) { + setPaymentMessage('缺少加项信息,请稍后重试'); + setPaymentLoading(false); + return; + } + + // 获取 patient_id(必须从接口返回的客户信息中获取) + if (!customerInfo?.patient_id) { + setPaymentMessage('缺少患者ID,请稍后重试'); + setPaymentLoading(false); + return; + } + const patient_id = customerInfo.patient_id; + + if (paymentMethod === 'self') { + // 自费模式:生成二维码 + const res = await createNativePaymentQrcode({ + physical_exam_id, + userName: customerInfo.customer_name!, + userPhone: customerInfo.phone!, + combinationItemCodes, + orderAmount: totalCurrent, + }); + + if (res.Status === 200 && res.Data) { + // res.Data 是微信支付 URL(如:weixin://wxpay/bizpayurl?pr=xxx) + // 需要将 URL 转换为二维码图片 + const paymentUrl = res.Data; + // 使用在线二维码生成 API 生成二维码图片 + const qrcodeImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(paymentUrl)}`; + setQrcodeUrl(qrcodeImageUrl); + setShowQrcodeModal(true); + // 开始轮询(自费模式:pay_type=12, company_id=0) + startPaymentPolling( + physical_exam_id, + patient_id, + combinationItemCodes, + discountRatio, + 12, // 微信支付 + 0 // 自费模式,company_id 传 0 + ); + } else { + setPaymentMessage(res.Message || '生成支付二维码失败'); + } + } else { + // 挂账模式:直接查询 + const selectedCompany = accountCompanyList.find( + (c) => c.company_name === accountCompany + ); + if (!selectedCompany) { + setPaymentMessage('请选择挂账公司'); + setPaymentLoading(false); + return; + } + + // 挂账模式:直接查询(pay_type=13, company_id=选中的公司ID) + const res = await checkNativePaymentStatus({ + physical_exam_id, + patient_id, + combinationItemCodes, + discount_rate: discountRatio, + pay_type: 13, // 挂账公司 + company_id: selectedCompany.company_id, + }); + + if (res.Status === 200) { + const result = res.Data; + if (result === 'success' || result === '1' || result === 'SUCCESS') { + setPaymentMessage('挂账成功,正在生成加项单...'); + setSelectedIds(new Set()); + // 获取加项PDF + fetchAddItemBillPdf(physical_exam_id).then((success) => { + if (success) { + setPaymentMessage('挂账成功,加项单已生成'); + } else { + setPaymentMessage('挂账成功,但加项单生成失败'); + } + setTimeout(() => { + setPaymentMessage(null); + }, 3000); + }); + } else { + setPaymentMessage('挂账失败'); + setTimeout(() => { + setPaymentMessage(null); + }, 3000); + } + } else { + setPaymentMessage(res.Message || '挂账失败'); + } + } + } catch (err) { + console.error('支付处理失败', err); + setPaymentMessage('支付处理失败,请稍后重试'); + } finally { + setPaymentLoading(false); + } + }; + return (
{/* 标题和说明 */} @@ -457,7 +741,7 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => { ¥{origPrice.toFixed(0)} )}
- ¥{currPrice.toFixed(0)} + ¥{currPrice.toFixed(2)} {getDiscountText(item)} @@ -474,12 +758,12 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
- 加项原价合计: ¥{totalOriginal.toFixed(0)} + 加项原价合计: ¥{totalOriginal.toFixed(2)}
- 渠道折扣价: ¥{totalCurrent.toFixed(0)} + 渠道折扣价: ¥{totalCurrent.toFixed(2)} {discount > 0 && ( - 已优惠 ¥{discount.toFixed(0)} + 已优惠 ¥{discount.toFixed(2)} )}
{/*
结算方式: 个人支付 (微信 / 支付宝)
*/} @@ -571,12 +855,54 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
+ {paymentMessage && ( +
+ {paymentMessage} +
+ )}
+ {/* 二维码支付弹窗 */} + {showQrcodeModal && qrcodeUrl && ( +
+
+
+
扫码支付
+ +
+
+
+ 支付二维码 +
+
+ 请使用微信扫描上方二维码完成支付 +
+ {paymentMessage && ( +
+ {paymentMessage} +
+ )} +
+
+
+ )}
); }; diff --git a/src/components/exam/ExamSignPanel.tsx b/src/components/exam/ExamSignPanel.tsx index 945e9a1..bce6eee 100644 --- a/src/components/exam/ExamSignPanel.tsx +++ b/src/components/exam/ExamSignPanel.tsx @@ -10,6 +10,7 @@ import { getTongyishuPdfList, setDaojiandanPdf, getDaojiandanPdf as getDaojiandanPdfFromStorage, + getAddItemBillPdf as getAddItemBillPdfFromStorage, type TongyishuPdfInfo, } from '../../utils/examActions'; import type { SignaturePadHandle } from '../ui'; @@ -65,6 +66,10 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { const [daojiandanSubmitLoading, setDaojiandanSubmitLoading] = useState(false); const [daojiandanSubmitMessage, setDaojiandanSubmitMessage] = useState(null); const [showDaojiandanPreview, setShowDaojiandanPreview] = useState(false); + // 加项单相关状态 + const [addItemBillUrl, setAddItemBillUrl] = useState(null); + const [addItemBillName, setAddItemBillName] = useState('加项单'); + const [showAddItemBillPreview, setShowAddItemBillPreview] = useState(false); const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading; @@ -301,6 +306,13 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { if (storedPdf && storedPdf.pdf_url) { setDaojiandanUrl(storedPdf.pdf_url); } + + // 检查加项单PDF + const storedAddItemBill = getAddItemBillPdfFromStorage(examId); + if (storedAddItemBill && storedAddItemBill.pdf_url) { + setAddItemBillUrl(storedAddItemBill.pdf_url); + setAddItemBillName(storedAddItemBill.pdf_name || '加项单'); + } }, [examId]); // 加载 PDF 数据 @@ -621,6 +633,100 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { } }; + // 加项单直接打印 + const handleAddItemBillDirectPrint = async () => { + if (!addItemBillUrl) return; + + try { + const response = await fetch(addItemBillUrl); + 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 = 1.2; + 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(` + + + + 加项单打印 + + + + `); + + canvasImages.forEach((imgData) => { + printWindow.document.write(``); + }); + + printWindow.document.write(` + + + `); + + printWindow.document.close(); + + printWindow.onload = () => { + printWindow.focus(); + setTimeout(() => { + printWindow.print(); + }, 1000); + }; + } catch (err) { + console.error('打印失败', err); + alert('打印失败,请稍后重试'); + } + }; + // 导检单直接打印 const handleDaojiandanDirectPrint = async () => { if (!daojiandanUrl) return; @@ -1108,6 +1214,42 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { )}
+ {/* 加项单 */} + {addItemBillUrl && ( +
+
+ {addItemBillName.length > 12 ? addItemBillName.slice(0, 12) + "..." : addItemBillName} + 已生成 +
+
+ + +
+
+ )} )} @@ -1283,6 +1425,36 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => { ) } + { + showAddItemBillPreview && addItemBillUrl && ( +
+
+
{addItemBillName}
+
+ + +
+
+
+
+