完善加项支付

This commit is contained in:
xianyi
2026-01-04 15:54:26 +08:00
parent f7ea59a857
commit f6cc55582e
5 changed files with 642 additions and 41 deletions

View File

@@ -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<string | null>(null);
const [paymentLoading, setPaymentLoading] = useState(false);
const [paymentMessage, setPaymentMessage] = useState<string | null>(null);
const pollingTimerRef = useRef<number | null>(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<Blob> => {
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 是微信支付 URLweixin://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 (
<div className='space-y-4'>
{/* 标题和说明 */}
@@ -457,7 +741,7 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
<span className='text-xs text-gray-400 line-through'>¥{origPrice.toFixed(0)}</span>
)}
<div className='flex items-center justify-between gap-2'>
<span className='text-[14px] font-bold text-red-600'>¥{currPrice.toFixed(0)}</span>
<span className='text-[14px] font-bold text-red-600'>¥{currPrice.toFixed(2)}</span>
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
{getDiscountText(item)}
</span>
@@ -474,12 +758,12 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
<div className='border-t pt-4 mt-6 flex items-center justify-between'>
<div className='space-y-1 text-sm'>
<div className='text-gray-600'>
: <span className='text-gray-900'>¥{totalOriginal.toFixed(0)}</span>
: <span className='text-gray-900'>¥{totalOriginal.toFixed(2)}</span>
</div>
<div className='text-gray-600'>
: <span className='text-xl font-bold text-red-600'>¥{totalCurrent.toFixed(0)}</span>
: <span className='text-xl font-bold text-red-600'>¥{totalCurrent.toFixed(2)}</span>
{discount > 0 && (
<span className='text-gray-500 ml-1'> ¥{discount.toFixed(0)}</span>
<span className='text-gray-500 ml-1'> ¥{discount.toFixed(2)}</span>
)}
</div>
{/* <div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</div> */}
@@ -571,12 +855,54 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
<Button
className='bg-[#269745] hover:bg-[#269745]/80 rounded-3xl text-white px-6 py-3 text-base font-medium'
disabled={selectedCount === 0}
disabled={selectedCount === 0 || paymentLoading}
onClick={handlePayment}
>
¥{totalCurrent.toFixed(0)}
{paymentLoading ? '处理中...' : `确认支付 ¥${totalCurrent.toFixed(2)}`}
</Button>
</div>
{paymentMessage && (
<div className={`text-sm text-center mt-2 ${paymentMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
{paymentMessage}
</div>
)}
</div>
{/* 二维码支付弹窗 */}
{showQrcodeModal && qrcodeUrl && (
<div className='fixed inset-0 z-[80] bg-black/80 flex items-center justify-center px-6'>
<div className='bg-white rounded-2xl w-full max-w-md shadow-2xl p-6 flex flex-col gap-4'>
<div className='flex items-center justify-between'>
<div className='text-lg font-semibold text-gray-900'></div>
<Button
className='py-1 px-3'
onClick={() => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setShowQrcodeModal(false);
setQrcodeUrl(null);
}}
>
</Button>
</div>
<div className='flex flex-col items-center gap-4'>
<div className='bg-white p-4 rounded-lg border-2 border-gray-200'>
<img src={qrcodeUrl} alt='支付二维码' className='w-64 h-64 object-contain' />
</div>
<div className='text-sm text-gray-600 text-center'>
使
</div>
{paymentMessage && (
<div className={`text-sm ${paymentMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
{paymentMessage}
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};