新增加项需求

This commit is contained in:
xianyi
2026-01-26 16:37:10 +08:00
parent 0d46ad034d
commit f6802a6e26
3 changed files with 569 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState, useRef } from 'react';
import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import type { ExamClient } from '../../data/mockData';
import { searchPhysicalExamAddItem, getAddItemCustomerInfo, getChannelCompanyList, createNativePaymentQrcode, checkNativePaymentStatus, getAddItemBillPdf } from '../../api';
import { searchPhysicalExamAddItem, getAddItemCustomerInfo, getChannelCompanyList, createNativePaymentQrcode, checkNativePaymentStatus, getAddItemBillPdf, getCustomSettlementApproveStatus, customSettlementApply, customSettlementApplyCancel } from '../../api';
import { Button, Input } from '../ui';
import { cls } from '../../utils/cls';
import nozImage from '../../assets/image/noz.png';
@@ -72,6 +72,24 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
const [paymentLoading, setPaymentLoading] = useState(false);
const [paymentMessage, setPaymentMessage] = useState<string | null>(null);
const pollingTimerRef = useRef<number | null>(null);
// 跟踪客户信息是否已加载(用于避免重复请求加项列表)
const customerInfoLoadedRef = useRef<boolean>(false);
// 自定义结算相关状态
const customSettlementPollingTimerRef = useRef<number | null>(null);
const lastFetchStatusTimeRef = useRef<number>(0); // 上次获取审批状态的时间戳
const [showCustomSettlementModal, setShowCustomSettlementModal] = useState(false);
const [customSettlementStatus, setCustomSettlementStatus] = useState<{
apply_status?: number;
apply_status_name?: string | null;
final_settlement_price?: number | null;
} | null>(null);
const [customSettlementLoading, setCustomSettlementLoading] = useState(false);
const [customSettlementType, setCustomSettlementType] = useState<1 | 2>(1); // 1-按比例折扣 2-自定义结算价
const [customDiscountRatio, setCustomDiscountRatio] = useState<number>(100); // 折扣比例如100代表10折即原价
const [customFinalPrice, setCustomFinalPrice] = useState<number>(0); // 最终结算价
const [customApplyReason, setCustomApplyReason] = useState<string>(''); // 申请理由
const [waitingSeconds, setWaitingSeconds] = useState<number>(0); // 等待审核的秒数
const waitingTimerRef = useRef<number | null>(null); // 等待计时器
// 点击外部关闭下拉框
useEffect(() => {
@@ -124,6 +142,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
scrm_account_id,
scrm_account_name,
});
customerInfoLoadedRef.current = true;
// 设置挂账公司默认值
const companyName = res.Data.customerInfo.company_name;
if (companyName && companyName.trim() !== '') {
@@ -149,9 +168,14 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
}
} catch (err) {
console.error('获取加项客户信息失败', err);
} finally {
// 无论成功或失败,都标记为已尝试加载
customerInfoLoadedRef.current = true;
}
};
// 当 client.id 变化时,重置加载状态
customerInfoLoadedRef.current = false;
fetchCustomerInfo();
}, [client.id]);
@@ -205,6 +229,10 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
// 拉取加项列表
useEffect(() => {
if (!customerInfoLoadedRef.current && customerInfo === null) {
return;
}
const fetchList = async () => {
setAddonLoading(true);
setAddonError(null);
@@ -249,7 +277,7 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
}
};
fetchList();
}, [debouncedAddonSearch, customerInfo?.scrm_account_id, customerInfo?.scrm_account_name, client.id]);
}, [debouncedAddonSearch, customerInfo?.scrm_account_id, customerInfo?.scrm_account_name, client.id, customerInfo]);
const allAddons = useMemo(() => addonList, [addonList]);
@@ -279,9 +307,15 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
const totalOriginal = selectedItems.reduce((sum, item) => {
return sum + parseFloat(item.originalPrice || item.currentPrice || '0');
}, 0);
const totalCurrent = selectedItems.reduce((sum, item) => {
const totalCurrentBase = selectedItems.reduce((sum, item) => {
return sum + parseFloat(item.currentPrice || item.originalPrice || '0');
}, 0);
// 如果自定义结算审核通过,使用审核后的金额
const totalCurrent = (customSettlementStatus?.apply_status === 3 && typeof customSettlementStatus.final_settlement_price === 'number')
? customSettlementStatus.final_settlement_price
: totalCurrentBase;
const discount = totalOriginal - totalCurrent;
// 获取标签样式
@@ -382,6 +416,277 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
};
}, []);
// 获取自定义结算审批状态(带节流,最多每秒一次)
const fetchCustomSettlementStatus = useCallback(async () => {
const physical_exam_id = Number(client.id);
const currentSelectedItems = allAddons.filter(item => selectedIds.has(item.id || item.name));
if (!physical_exam_id || currentSelectedItems.length === 0) {
setCustomSettlementStatus(null);
return false;
}
const add_item_id = currentSelectedItems
.map(item => item.combinationItemCode)
.filter((code): code is number => code !== null && code !== undefined)
.join(',');
if (!add_item_id) {
setCustomSettlementStatus(null);
return false;
}
// 节流如果距离上次调用不足0.8秒,则跳过
const now = Date.now();
const timeSinceLastCall = now - lastFetchStatusTimeRef.current;
if (timeSinceLastCall < 800) {
// 返回当前状态是否需要轮询
return customSettlementStatus?.apply_status === 1;
}
lastFetchStatusTimeRef.current = now;
try {
const res = await getCustomSettlementApproveStatus({
physical_exam_id,
add_item_id,
});
if (res.Status === 200 && res.Data) {
const status = {
apply_status: res.Data.apply_status,
apply_status_name: res.Data.apply_status_name,
final_settlement_price: res.Data.final_settlement_price,
};
setCustomSettlementStatus(status);
// 如果状态变为审核中,重置等待时间
if (res.Data.apply_status === 1 && customSettlementStatus?.apply_status !== 1) {
setWaitingSeconds(0);
}
// 返回是否需要继续轮询1-审核中 需要轮询)
return res.Data.apply_status === 1;
} else {
setCustomSettlementStatus(null);
return false;
}
} catch (err) {
console.error('获取自定义结算审批状态失败', err);
setCustomSettlementStatus(null);
return false;
}
}, [client.id, selectedIds, allAddons, customSettlementStatus]);
// 当选中加项变化时,获取审批状态
useEffect(() => {
const currentSelectedItems = allAddons.filter(item => selectedIds.has(item.id || item.name));
if (currentSelectedItems.length > 0) {
fetchCustomSettlementStatus().then((shouldPoll) => {
// 如果需要轮询(审核中),开始轮询
if (shouldPoll) {
startCustomSettlementPolling();
} else {
stopCustomSettlementPolling();
}
});
} else {
setCustomSettlementStatus(null);
stopCustomSettlementPolling();
}
}, [selectedIds, client.id, allAddons, fetchCustomSettlementStatus]);
// 开始轮询自定义结算审批状态
const startCustomSettlementPolling = useCallback(() => {
// 清除之前的定时器
stopCustomSettlementPolling();
// 每1秒轮询一次
customSettlementPollingTimerRef.current = window.setInterval(() => {
fetchCustomSettlementStatus().then((shouldPoll) => {
// 如果不再需要轮询(审核完成),停止轮询
if (!shouldPoll) {
stopCustomSettlementPolling();
}
});
}, 1000);
}, [fetchCustomSettlementStatus]);
// 停止轮询自定义结算审批状态
const stopCustomSettlementPolling = useCallback(() => {
if (customSettlementPollingTimerRef.current) {
clearInterval(customSettlementPollingTimerRef.current);
customSettlementPollingTimerRef.current = null;
}
}, []);
// 清理轮询定时器
useEffect(() => {
return () => {
stopCustomSettlementPolling();
};
}, [stopCustomSettlementPolling]);
// 等待计时器:当审核中时开始计时
useEffect(() => {
if (customSettlementStatus?.apply_status === 1) {
// 开始计时
setWaitingSeconds(0);
waitingTimerRef.current = window.setInterval(() => {
setWaitingSeconds((prev) => prev + 1);
}, 1000);
} else {
// 停止计时并重置
if (waitingTimerRef.current) {
clearInterval(waitingTimerRef.current);
waitingTimerRef.current = null;
}
setWaitingSeconds(0);
}
return () => {
if (waitingTimerRef.current) {
clearInterval(waitingTimerRef.current);
waitingTimerRef.current = null;
}
};
}, [customSettlementStatus?.apply_status]);
// 提交自定义结算申请
const handleSubmitCustomSettlement = async () => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id || selectedItems.length === 0) {
setPaymentMessage('请先选择加项项目');
return;
}
if (!customApplyReason.trim()) {
setPaymentMessage('请输入申请理由');
return;
}
setCustomSettlementLoading(true);
setPaymentMessage(null);
try {
// 构建加项项目明细列表
const listAddItemDetail = selectedItems
.map(item => {
const combinationItemCode = item.combinationItemCode;
if (combinationItemCode === null || combinationItemCode === undefined) {
return null;
}
const originalPrice = parseFloat(item.originalPrice || '0');
let settlementPrice = originalPrice;
if (customSettlementType === 1) {
// 按比例折扣
settlementPrice = originalPrice * (customDiscountRatio / 100);
} else {
// 自定义结算价
settlementPrice = customFinalPrice / selectedItems.length; // 平均分配
}
return {
combination_item_code: String(combinationItemCode),
combination_item_name: item.name,
original_price: originalPrice,
settlement_price: settlementPrice,
};
})
.filter((item): item is { combination_item_code: string; combination_item_name: string; original_price: number; settlement_price: number } => item !== null);
const original_settlement_price = selectedItems.reduce((sum, item) => {
return sum + parseFloat(item.currentPrice || item.originalPrice || '0');
}, 0);
const final_settlement_price = customSettlementType === 1
? original_settlement_price * (customDiscountRatio / 100)
: customFinalPrice;
const res = await customSettlementApply({
physical_exam_id,
listAddItemDetail,
original_settlement_price,
settlement_type: customSettlementType,
discount_ratio: customSettlementType === 1 ? customDiscountRatio : undefined,
final_settlement_price,
apply_reason: customApplyReason.trim(),
});
if (res.Status === 200) {
setPaymentMessage('自定义结算申请提交成功');
setShowCustomSettlementModal(false);
// 重置表单
setCustomSettlementType(1);
setCustomDiscountRatio(100);
setCustomFinalPrice(0);
setCustomApplyReason('');
// 重新获取审批状态并开始轮询
setTimeout(() => {
fetchCustomSettlementStatus().then((shouldPoll) => {
if (shouldPoll) {
startCustomSettlementPolling();
}
});
}, 500);
} else {
setPaymentMessage(res.Message || '提交失败,请稍后重试');
}
} catch (err) {
console.error('提交自定义结算申请失败', err);
setPaymentMessage('提交失败,请稍后重试');
} finally {
setCustomSettlementLoading(false);
}
};
// 取消自定义结算申请
const handleCancelCustomSettlement = async () => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id || selectedItems.length === 0) {
setPaymentMessage('请先选择加项项目');
return;
}
if (!window.confirm('确定要取消自定义结算申请吗?')) {
return;
}
setCustomSettlementLoading(true);
setPaymentMessage(null);
try {
const add_item_id = selectedItems
.map(item => item.combinationItemCode)
.filter((code): code is number => code !== null && code !== undefined)
.join(',');
if (!add_item_id) {
setPaymentMessage('无法获取加项项目ID');
return;
}
const res = await customSettlementApplyCancel({
physical_exam_id,
add_item_id,
});
if (res.Status === 200 && res.Data?.is_success === 1) {
setPaymentMessage('取消申请成功');
// 停止轮询
stopCustomSettlementPolling();
// 重新获取审批状态
setTimeout(() => {
fetchCustomSettlementStatus();
}, 500);
} else {
setPaymentMessage(res.Message || '取消申请失败,请稍后重试');
}
} catch (err) {
console.error('取消自定义结算申请失败', err);
setPaymentMessage('取消申请失败,请稍后重试');
} finally {
setCustomSettlementLoading(false);
}
};
// 获取加项PDF按本次支付的组合代码生成对应的加项单
const fetchAddItemBillPdf = async (examId: number, combinationItemCodes: string) => {
try {
@@ -797,9 +1102,46 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
{/* <div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</div> */}
</div>
{/* 添加自定义结算 */}
{localStorage.getItem('authCode')?.includes('HisTijianPad_Btn_Tongji') && selectedItems.length > 0 && (
<div className='flex flex-col items-end gap-2 relative'>
{customSettlementStatus && (
<div className='text-xs text-gray-600'>
: <span className={cls(
'font-semibold',
customSettlementStatus.apply_status === 1 && 'text-blue-600', // 审核中
customSettlementStatus.apply_status === 3 && 'text-green-600', // 审核通过
customSettlementStatus.apply_status === 4 && 'text-red-600', // 审核不通过
customSettlementStatus.apply_status === 2 && 'text-gray-600' // 取消申请
)}>
{customSettlementStatus.apply_status_name || '未知'}
{customSettlementStatus.apply_status === 1 && ' (审核中...)'}
</span>
{customSettlementStatus.final_settlement_price !== null && customSettlementStatus.final_settlement_price !== undefined && (
<span className='ml-2'>
: <span className='font-semibold text-red-600'>¥{customSettlementStatus.final_settlement_price.toFixed(2)}</span>
</span>
)}
</div>
)}
<Button
onClick={() => setShowCustomSettlementModal(true)}
disabled={customSettlementStatus?.apply_status === 1}
className={cls(
'px-3 py-1.5 text-xs text-white',
customSettlementStatus?.apply_status === 1
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
)}
>
</Button>
</div>
)}
{paymentMessage && (
<div className={`text-sm text-center mt-2 ${paymentMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
{paymentMessage}
</div>
)}
@@ -959,6 +1301,191 @@ export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
</div>
</div>
)}
{/* 自定义结算弹窗 */}
{showCustomSettlementModal && (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/30' onClick={() => setShowCustomSettlementModal(false)}>
<div
className='w-[500px] max-w-[95vw] bg-white rounded-2xl shadow-xl overflow-hidden'
onClick={(e) => e.stopPropagation()}
>
<div className='px-4 py-3 border-b flex items-center justify-between'>
<div className='font-semibold'></div>
<button
className='text-xs text-gray-500 hover:text-gray-700'
onClick={() => setShowCustomSettlementModal(false)}
>
</button>
</div>
<div className='px-4 py-6 space-y-4'>
{/* 选中项目信息 */}
<div className='text-sm text-gray-700'>
<div className='font-medium mb-2'> ({selectedItems.length}):</div>
<div className='max-h-32 overflow-y-auto space-y-1'>
{selectedItems.map((item, idx) => (
<div key={idx} className='text-xs text-gray-600 flex justify-between'>
<span>{item.name}</span>
<span>¥{parseFloat(item.currentPrice || item.originalPrice || '0').toFixed(2)}</span>
</div>
))}
</div>
<div className='mt-2 pt-2 border-t text-sm font-semibold flex justify-between'>
<span>:</span>
<span className='text-red-600'>¥{totalCurrent.toFixed(2)}</span>
</div>
</div>
{/* 结算方式 */}
<div className='space-y-2'>
<label className='text-sm text-gray-700 font-medium'></label>
<div className='flex gap-3'>
<button
type='button'
onClick={() => setCustomSettlementType(1)}
className={cls(
'flex-1 px-4 py-2 rounded-lg border text-sm transition-colors',
customSettlementType === 1
? 'bg-blue-50 border-blue-500 text-blue-700 font-medium'
: 'bg-white border-gray-300 text-gray-700 hover:border-gray-400'
)}
>
</button>
<button
type='button'
onClick={() => setCustomSettlementType(2)}
className={cls(
'flex-1 px-4 py-2 rounded-lg border text-sm transition-colors',
customSettlementType === 2
? 'bg-blue-50 border-blue-500 text-blue-700 font-medium'
: 'bg-white border-gray-300 text-gray-700 hover:border-gray-400'
)}
>
</button>
</div>
</div>
{/* 折扣比例或最终结算价 */}
{customSettlementType === 1 ? (
<div className='space-y-3'>
<div className='flex items-center justify-between'>
<label className='text-sm text-gray-700 font-medium'></label>
<span className='text-sm font-semibold text-blue-600'>{customDiscountRatio / 10}</span>
</div>
<div className='relative'>
<input
type='range'
min='10'
max='100'
step='5'
value={customDiscountRatio}
onChange={(e) => {
setCustomDiscountRatio(Number(e.target.value));
}}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600'
style={{
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${((customDiscountRatio - 10) / (100 - 10)) * 100}%, #e5e7eb ${((customDiscountRatio - 10) / (100 - 10)) * 100}%, #e5e7eb 100%)`
}}
/>
{/* 刻度标记 */}
<div className='flex justify-between mt-1 px-1'>
{[10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100].map((value) => (
<span
key={value}
className='text-[10px] text-gray-400'
style={{ width: '10px' }}
>
{value / 10}
</span>
))}
</div>
</div>
<div className='text-xs text-gray-500'>
: <span className='font-semibold text-red-600'>¥{(totalCurrent * (customDiscountRatio / 100)).toFixed(2)}</span>
</div>
</div>
) : (
<div className='space-y-2'>
<label className='text-sm text-gray-700 font-medium'> (¥)</label>
<Input
type='number'
min='0'
step='0.01'
value={customFinalPrice || ''}
onChange={(e) => {
const val = Number(e.target.value);
if (val >= 0) {
setCustomFinalPrice(val);
}
}}
placeholder='请输入最终结算价'
className='text-sm'
/>
</div>
)}
{/* 申请理由 */}
<div className='space-y-2'>
<label className='text-sm text-gray-700 font-medium'> *</label>
<textarea
value={customApplyReason}
onChange={(e) => setCustomApplyReason(e.target.value)}
placeholder='请输入申请理由'
rows={3}
className='w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 resize-none'
/>
</div>
{/* 提交按钮 */}
<div className='flex gap-3 pt-2'>
<Button
onClick={() => setShowCustomSettlementModal(false)}
className='flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700'
>
</Button>
<Button
onClick={handleSubmitCustomSettlement}
disabled={customSettlementLoading || !customApplyReason.trim()}
className={cls(
'flex-1 px-4 py-2 text-white font-medium',
customSettlementLoading || !customApplyReason.trim()
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
)}
>
{customSettlementLoading ? '提交中...' : '提交申请'}
</Button>
</div>
</div>
</div>
</div>
)}
{/* 审核中全屏遮罩 */}
{customSettlementStatus?.apply_status === 1 && (
<div className='fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50'>
<div className='bg-white rounded-2xl p-6 max-w-md w-full mx-4 shadow-xl'>
<div className='text-center mb-6'>
<div className='text-lg font-semibold text-blue-600 mb-2'>...</div>
<div className='text-sm text-gray-500 mb-1'></div>
<div className='text-xs text-gray-400'> {waitingSeconds} </div>
</div>
<div className='flex justify-center'>
<Button
onClick={handleCancelCustomSettlement}
disabled={customSettlementLoading}
className='px-6 py-2 bg-gray-500 hover:bg-gray-600 text-white disabled:opacity-50'
>
{customSettlementLoading ? '取消中...' : '取消申请'}
</Button>
</div>
</div>
</div>
)}
</div>
);
};