完善加项支付
This commit is contained in:
@@ -22,6 +22,8 @@ import type {
|
|||||||
DaojiandanGetResponse,
|
DaojiandanGetResponse,
|
||||||
InputDaojiandanSignSubmit,
|
InputDaojiandanSignSubmit,
|
||||||
DaojiandanSignSubmitResponse,
|
DaojiandanSignSubmitResponse,
|
||||||
|
InputAddItemBillSignSubmit,
|
||||||
|
AddItemBillSignSubmitResponse,
|
||||||
InputDaojiandanPrintStatus,
|
InputDaojiandanPrintStatus,
|
||||||
DaojiandanPrintStatusResponse,
|
DaojiandanPrintStatusResponse,
|
||||||
InputCustomerDetailEdit,
|
InputCustomerDetailEdit,
|
||||||
@@ -304,6 +306,26 @@ export const submitDaojiandanSign = (
|
|||||||
).then(res => res.data);
|
).then(res => res.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交体检加项单签名生成PDF
|
||||||
|
*/
|
||||||
|
export const submitAddItemBillSign = (
|
||||||
|
data: InputAddItemBillSignSubmit
|
||||||
|
): Promise<AddItemBillSignSubmitResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('sign_file', data.sign_file);
|
||||||
|
|
||||||
|
return request.post<AddItemBillSignSubmitResponse>(
|
||||||
|
`${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);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 体检导检单打印状态修改
|
* 体检导检单打印状态修改
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -400,6 +400,33 @@ export interface OutputDaojiandanSignInfo {
|
|||||||
*/
|
*/
|
||||||
export type DaojiandanSignSubmitResponse = CommonActionResult<OutputDaojiandanSignInfo>;
|
export type DaojiandanSignSubmitResponse = CommonActionResult<OutputDaojiandanSignInfo>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交体检加项单签名入参
|
||||||
|
*/
|
||||||
|
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<OutputAddItemBillSignInfo>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 体检导检单打印状态修改入参
|
* 体检导检单打印状态修改入参
|
||||||
*/
|
*/
|
||||||
@@ -457,6 +484,8 @@ export interface InputPhysicalExamAddItem {
|
|||||||
* 加项界面客户基本信息
|
* 加项界面客户基本信息
|
||||||
*/
|
*/
|
||||||
export interface PoAddItemCustomerInfo {
|
export interface PoAddItemCustomerInfo {
|
||||||
|
/** 患者ID */
|
||||||
|
patient_id: number;
|
||||||
/** 客户姓名 */
|
/** 客户姓名 */
|
||||||
customer_name?: string | null;
|
customer_name?: string | null;
|
||||||
/** 体检编号 */
|
/** 体检编号 */
|
||||||
@@ -595,16 +624,10 @@ export interface InputPhysicalExamAddOrder {
|
|||||||
userName: string;
|
userName: string;
|
||||||
/** 用户手机号 */
|
/** 用户手机号 */
|
||||||
userPhone: string;
|
userPhone: string;
|
||||||
/** 体检加项产品ID列表(逗号分隔) */
|
/** 体检组合项目代码(addItemList.combination_item_code 多个加项逗号分隔) */
|
||||||
addItemIds: string;
|
combinationItemCodes: string;
|
||||||
/** 微信Native产品ID */
|
/** 订单总金额(元) */
|
||||||
nativeProductId: string;
|
|
||||||
/** 订单金额(元) */
|
|
||||||
orderAmount: number;
|
orderAmount: number;
|
||||||
/** 订单来源(mini_program-小程序,app-app应用等) */
|
|
||||||
source: string;
|
|
||||||
/** 商户ID(可选,不传则使用默认商户) */
|
|
||||||
merchantId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -618,8 +641,16 @@ export type PhysicalExamQrcodeCreateResponse = CommonActionResult<string>;
|
|||||||
export interface InputOrderPaymentInfo {
|
export interface InputOrderPaymentInfo {
|
||||||
/** 体检ID */
|
/** 体检ID */
|
||||||
physical_exam_id: number;
|
physical_exam_id: number;
|
||||||
/** 订单编号 */
|
/** 患者ID */
|
||||||
order_id: string;
|
patient_id: number;
|
||||||
|
/** 体检组合项目代码(addItemList.combination_item_code 多个加项逗号分隔) */
|
||||||
|
combinationItemCodes: string;
|
||||||
|
/** 折扣比例 */
|
||||||
|
discount_rate: number;
|
||||||
|
/** 支付方式(12-微信 13-挂账公司) */
|
||||||
|
pay_type: number;
|
||||||
|
/** 挂账公司ID(挂账公司传对应的ID,其他传0) */
|
||||||
|
company_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState, useRef } from 'react';
|
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||||
|
|
||||||
import type { ExamClient } from '../../data/mockData';
|
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 { Button, Input } from '../ui';
|
||||||
import { cls } from '../../utils/cls';
|
import { cls } from '../../utils/cls';
|
||||||
import nozImage from '../../assets/image/noz.png';
|
import nozImage from '../../assets/image/noz.png';
|
||||||
|
import { setAddItemBillPdf } from '../../utils/examActions';
|
||||||
|
|
||||||
interface AddonTag {
|
interface AddonTag {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -18,6 +19,7 @@ interface AddonItem {
|
|||||||
tags?: AddonTag[];
|
tags?: AddonTag[];
|
||||||
originalPrice?: string;
|
originalPrice?: string;
|
||||||
currentPrice?: string;
|
currentPrice?: string;
|
||||||
|
combinationItemCode?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExamAddonPanelProps {
|
interface ExamAddonPanelProps {
|
||||||
@@ -54,6 +56,18 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
const [accountCompanyList, setAccountCompanyList] = useState<
|
const [accountCompanyList, setAccountCompanyList] = useState<
|
||||||
Array<{ company_id: number; company_name?: string | null; pinyin?: string | null }>
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -86,11 +100,24 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
const fetchCustomerInfo = async () => {
|
const fetchCustomerInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getAddItemCustomerInfo({ physical_exam_id });
|
const res = await getAddItemCustomerInfo({ physical_exam_id });
|
||||||
if (res.Status === 200 && res.Data?.listChannelDiscount && res.Data.listChannelDiscount.length > 0) {
|
if (res.Status === 200) {
|
||||||
setChannelDiscounts(res.Data.listChannelDiscount);
|
// 保存客户信息
|
||||||
const rate = res.Data.listChannelDiscount[0]?.discount_rate;
|
if (res.Data?.customerInfo) {
|
||||||
if (typeof rate === 'number' && rate > 0) {
|
setCustomerInfo({
|
||||||
setDiscountRatio(rate);
|
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 {
|
} else {
|
||||||
setChannelDiscounts([]);
|
setChannelDiscounts([]);
|
||||||
@@ -182,6 +209,7 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
: item.original_price !== undefined && item.original_price !== null
|
: item.original_price !== undefined && item.original_price !== null
|
||||||
? Number(item.original_price).toFixed(2)
|
? Number(item.original_price).toFixed(2)
|
||||||
: '0.00',
|
: '0.00',
|
||||||
|
combinationItemCode: item.combination_item_code ?? null,
|
||||||
tags: [],
|
tags: [],
|
||||||
paid: false,
|
paid: false,
|
||||||
}));
|
}));
|
||||||
@@ -318,6 +346,262 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
}));
|
}));
|
||||||
}, [accountCompanyList]);
|
}, [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 是微信支付 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 (
|
return (
|
||||||
<div className='space-y-4'>
|
<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>
|
<span className='text-xs text-gray-400 line-through'>¥{origPrice.toFixed(0)}</span>
|
||||||
)}
|
)}
|
||||||
<div className='flex items-center justify-between gap-2'>
|
<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`}>
|
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
|
||||||
{getDiscountText(item)}
|
{getDiscountText(item)}
|
||||||
</span>
|
</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='border-t pt-4 mt-6 flex items-center justify-between'>
|
||||||
<div className='space-y-1 text-sm'>
|
<div className='space-y-1 text-sm'>
|
||||||
<div className='text-gray-600'>
|
<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>
|
||||||
<div className='text-gray-600'>
|
<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 && (
|
{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>
|
||||||
{/* <div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</div> */}
|
{/* <div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</div> */}
|
||||||
@@ -571,12 +855,54 @@ export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
className='bg-[#269745] hover:bg-[#269745]/80 rounded-3xl text-white px-6 py-3 text-base font-medium'
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{paymentMessage && (
|
||||||
|
<div className={`text-sm text-center mt-2 ${paymentMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
|
||||||
|
{paymentMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getTongyishuPdfList,
|
getTongyishuPdfList,
|
||||||
setDaojiandanPdf,
|
setDaojiandanPdf,
|
||||||
getDaojiandanPdf as getDaojiandanPdfFromStorage,
|
getDaojiandanPdf as getDaojiandanPdfFromStorage,
|
||||||
|
getAddItemBillPdf as getAddItemBillPdfFromStorage,
|
||||||
type TongyishuPdfInfo,
|
type TongyishuPdfInfo,
|
||||||
} from '../../utils/examActions';
|
} from '../../utils/examActions';
|
||||||
import type { SignaturePadHandle } from '../ui';
|
import type { SignaturePadHandle } from '../ui';
|
||||||
@@ -65,6 +66,10 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
|
|||||||
const [daojiandanSubmitLoading, setDaojiandanSubmitLoading] = useState(false);
|
const [daojiandanSubmitLoading, setDaojiandanSubmitLoading] = useState(false);
|
||||||
const [daojiandanSubmitMessage, setDaojiandanSubmitMessage] = useState<string | null>(null);
|
const [daojiandanSubmitMessage, setDaojiandanSubmitMessage] = useState<string | null>(null);
|
||||||
const [showDaojiandanPreview, setShowDaojiandanPreview] = useState(false);
|
const [showDaojiandanPreview, setShowDaojiandanPreview] = useState(false);
|
||||||
|
// 加项单相关状态
|
||||||
|
const [addItemBillUrl, setAddItemBillUrl] = useState<string | null>(null);
|
||||||
|
const [addItemBillName, setAddItemBillName] = useState<string>('加项单');
|
||||||
|
const [showAddItemBillPreview, setShowAddItemBillPreview] = useState(false);
|
||||||
|
|
||||||
const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading;
|
const busy = signLoading || submitLoading || consentLoading || pdfLoading || daojiandanSubmitLoading;
|
||||||
|
|
||||||
@@ -301,6 +306,13 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
|
|||||||
if (storedPdf && storedPdf.pdf_url) {
|
if (storedPdf && storedPdf.pdf_url) {
|
||||||
setDaojiandanUrl(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]);
|
}, [examId]);
|
||||||
|
|
||||||
// 加载 PDF 数据
|
// 加载 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(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>加项单打印</title>
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
page-break-after: always;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
img:last-child {
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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 () => {
|
const handleDaojiandanDirectPrint = async () => {
|
||||||
if (!daojiandanUrl) return;
|
if (!daojiandanUrl) return;
|
||||||
@@ -1108,6 +1214,42 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 加项单 */}
|
||||||
|
{addItemBillUrl && (
|
||||||
|
<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'>{addItemBillName.length > 12 ? addItemBillName.slice(0, 12) + "..." : addItemBillName}</span>
|
||||||
|
<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'>
|
||||||
|
<Button
|
||||||
|
className='py-1.5 px-3 bg-blue-600 hover:bg-blue-700 text-white'
|
||||||
|
onClick={() => {
|
||||||
|
if (busy) return;
|
||||||
|
handleAddItemBillDirectPrint();
|
||||||
|
}}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
打印
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='py-1.5 px-3'
|
||||||
|
onClick={() => {
|
||||||
|
if (busy) return;
|
||||||
|
setShowAddItemBillPreview(true);
|
||||||
|
}}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1283,6 +1425,36 @@ export const ExamSignPanel = ({ examId, onBusyChange }: ExamSignPanelProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
showAddItemBillPreview && addItemBillUrl && (
|
||||||
|
<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'>{addItemBillName}</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={handleAddItemBillDirectPrint}
|
||||||
|
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'>
|
||||||
|
<div className='flex justify-center p-4'>
|
||||||
|
<iframe
|
||||||
|
src={addItemBillUrl}
|
||||||
|
className='w-full h-full min-h-[600px] border rounded-lg bg-white'
|
||||||
|
title='加项单预览'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -188,3 +188,53 @@ export const getDaojiandanPdf = (examId: string | number): DaojiandanPdfInfo | n
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加项单PDF信息
|
||||||
|
*/
|
||||||
|
export interface AddItemBillPdfInfo {
|
||||||
|
/** PDF文件名称 */
|
||||||
|
pdf_name: string;
|
||||||
|
/** PDF文件地址 */
|
||||||
|
pdf_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加项单PDF的存储 key
|
||||||
|
*/
|
||||||
|
export const getAddItemBillPdfKey = (examId: string | number): string => {
|
||||||
|
const today = getTodayString();
|
||||||
|
return `yh_add_item_bill_pdf_${today}_${examId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储加项单PDF信息
|
||||||
|
*/
|
||||||
|
export const setAddItemBillPdf = (
|
||||||
|
examId: string | number,
|
||||||
|
pdfInfo: AddItemBillPdfInfo
|
||||||
|
): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const key = getAddItemBillPdfKey(examId);
|
||||||
|
localStorage.setItem(key, JSON.stringify(pdfInfo));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加项单PDF信息
|
||||||
|
*/
|
||||||
|
export const getAddItemBillPdf = (examId: string | number): AddItemBillPdfInfo | null => {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
|
const key = getAddItemBillPdfKey(examId);
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return parsed as AddItemBillPdfInfo;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('加项单PDF信息解析失败', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user