Files
ipad/src/components/exam/ExamAddonPanel.tsx
2026-02-02 16:14:55 +08:00

1621 lines
63 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import type { ExamClient } from '../../data/mockData';
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';
interface AddonTag {
title: string;
type: 1 | 2 | 3 | 4; // 1: 热门(红), 2: 普通(灰), 3: 医生推荐(蓝), 4: 折扣信息
}
interface AddonItem {
id?: string;
name: string;
paid?: boolean;
tags?: AddonTag[];
originalPrice?: string;
currentPrice?: string;
combinationItemCode?: number | null;
isEnjoyDiscount?: number | null;
discount_name?: string | null;
}
interface ExamAddonPanelProps {
client: ExamClient;
onGoToSign?: () => void;
}
export const ExamAddonPanel = ({ client, onGoToSign }: ExamAddonPanelProps) => {
const [addonList, setAddonList] = useState<AddonItem[]>([]);
const allAddons = useMemo(() => addonList, [addonList]);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const currentAddItemId = useMemo(() => {
const currentSelectedItems = allAddons.filter(item => selectedIds.has(item.id || item.name));
return currentSelectedItems
.map(item => item.combinationItemCode)
.filter((code): code is number => code !== null && code !== undefined)
.join(',');
}, [allAddons, selectedIds]);
// 防抖:内部输入值(用于显示)
const [addonSearchInput, setAddonSearchInput] = useState('');
// 防抖:实际用于 API 调用的值(延迟更新)
const [debouncedAddonSearch, setDebouncedAddonSearch] = useState('');
const debounceTimerRef = useRef<number | null>(null);
const [addonLoading, setAddonLoading] = useState(false);
const [addonError, setAddonError] = useState<string | null>(null);
// 折扣比例1 = 100%
const [discountRatio, setDiscountRatio] = useState<number>(1);
// 渠道折扣列表
const [channelDiscounts, setChannelDiscounts] = useState<
{ channel_id?: string | null; channel_name?: string | null; discount_rate?: number | null; discount_name?: string | null }[]
>([]);
// 下拉框状态
const [isDiscountDropdownOpen, setIsDiscountDropdownOpen] = useState(false);
const discountDropdownRef = useRef<HTMLDivElement>(null);
// 结算方式:'self' 自费, 'account' 挂账
const [paymentMethod, setPaymentMethod] = useState<'self' | 'account'>('self');
const [isPaymentMethodDropdownOpen, setIsPaymentMethodDropdownOpen] = useState(false);
const paymentMethodDropdownRef = useRef<HTMLDivElement>(null);
// 挂账公司
const [accountCompany, setAccountCompany] = useState<string>('');
const [isAccountCompanyDropdownOpen, setIsAccountCompanyDropdownOpen] = useState(false);
const [accountCompanySearch, setAccountCompanySearch] = useState('');
const accountCompanyDropdownRef = useRef<HTMLDivElement>(null);
// 挂账公司列表
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;
scrm_account_id?: string | null;
scrm_account_name?: string | 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);
// 跟踪客户信息是否已加载(用于避免重复请求加项列表)
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;
apply_reason?: string | null;
settlement_type?: number | null;
discount_ratio?: number | null;
add_item_id?: string | null;
} | null>(null);
const [customSettlementLoading, setCustomSettlementLoading] = useState(false);
const [customSettlementType, setCustomSettlementType] = useState<1 | 2>(1); // 1-按比例折扣 2-自定义结算价
const [customDiscountRatio, setCustomDiscountRatio] = useState<number | null>(null); // 折扣比例如100代表10折即原价
const [customFinalPrice, setCustomFinalPrice] = useState<number | null>(null); // 最终结算价
const [customApplyReason, setCustomApplyReason] = useState<string>(''); // 申请理由
const [waitingSeconds, setWaitingSeconds] = useState<number>(0); // 等待审核的秒数
const waitingTimerRef = useRef<number | null>(null); // 等待计时器
useEffect(() => {
if (showCustomSettlementModal && customSettlementStatus) {
// 已通过(3) 或 已拒绝(4) 且 add_item_id 匹配才需要回显
if ((customSettlementStatus.apply_status === 3 || customSettlementStatus.apply_status === 4) &&
customSettlementStatus.add_item_id === currentAddItemId) {
// 如果 discount_ratio 为 0说明是自定义结算价模式
if (customSettlementStatus.discount_ratio === 0) {
setCustomSettlementType(2);
} else if (customSettlementStatus.settlement_type === 1 || customSettlementStatus.settlement_type === 2) {
setCustomSettlementType(customSettlementStatus.settlement_type as 1 | 2);
}
if (typeof customSettlementStatus.discount_ratio === 'number') {
setCustomDiscountRatio(customSettlementStatus.discount_ratio || null);
}
if (typeof customSettlementStatus.final_settlement_price === 'number') {
setCustomFinalPrice(customSettlementStatus.final_settlement_price);
}
if (customSettlementStatus.apply_reason) {
setCustomApplyReason(customSettlementStatus.apply_reason);
}
} else {
// 其他状态(如取消后再点开)重置表单为默认值
setCustomSettlementType(1);
setCustomDiscountRatio(null);
setCustomFinalPrice(null);
setCustomApplyReason('');
}
}
}, [showCustomSettlementModal, customSettlementStatus, currentAddItemId]);
// 点击外部关闭下拉框
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (discountDropdownRef.current && !discountDropdownRef.current.contains(event.target as Node)) {
setIsDiscountDropdownOpen(false);
}
if (paymentMethodDropdownRef.current && !paymentMethodDropdownRef.current.contains(event.target as Node)) {
setIsPaymentMethodDropdownOpen(false);
}
if (accountCompanyDropdownRef.current && !accountCompanyDropdownRef.current.contains(event.target as Node)) {
setIsAccountCompanyDropdownOpen(false);
}
};
if (isDiscountDropdownOpen || isPaymentMethodDropdownOpen || isAccountCompanyDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isDiscountDropdownOpen, isPaymentMethodDropdownOpen, isAccountCompanyDropdownOpen]);
// 获取体检加项客户信息(用于拿到渠道折扣等信息)
useEffect(() => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id) return;
const fetchCustomerInfo = async () => {
try {
const res = await getAddItemCustomerInfo({ physical_exam_id });
if (res.Status === 200) {
// 保存客户信息,渠道信息使用 listChannelDiscount 中的 channel_id / channel_name
if (res.Data?.customerInfo) {
const firstChannel = res.Data.listChannelDiscount?.[0];
setCustomerInfo({
patient_id: res.Data.customerInfo.patient_id,
customer_name: res.Data.customerInfo.customer_name,
phone: res.Data.customerInfo.phone,
scrm_account_id: firstChannel?.channel_id ?? null,
scrm_account_name: firstChannel?.channel_name ?? null,
});
customerInfoLoadedRef.current = true;
// 设置挂账公司默认值
const companyName = res.Data.customerInfo.company_name;
if (companyName && companyName.trim() !== '') {
setAccountCompany(companyName);
setAccountCompanySearch(companyName);
} else {
setAccountCompany('');
setAccountCompanySearch('');
}
}
// 保存渠道折扣信息
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([]);
}
} catch (err) {
console.error('获取加项客户信息失败', err);
} finally {
// 无论成功或失败,都标记为已尝试加载
customerInfoLoadedRef.current = true;
}
};
// 当 client.id 变化时,重置加载状态
customerInfoLoadedRef.current = false;
fetchCustomerInfo();
}, [client.id]);
// 获取挂账公司列表
useEffect(() => {
const fetchCompanyList = async () => {
try {
const res = await getChannelCompanyList({});
if (res.Status === 200 && Array.isArray(res.Data) && res.Data.length > 0) {
setAccountCompanyList(res.Data);
} else {
setAccountCompanyList([]);
}
} catch (err) {
console.error('获取挂账公司列表失败', err);
setAccountCompanyList([]);
}
};
fetchCompanyList();
}, []);
// 当挂账公司列表更新时,如果当前选中的公司不在列表中,清空选择
useEffect(() => {
if (accountCompany && accountCompanyList.length > 0) {
const companyNames = accountCompanyList.map((c) => c.company_name).filter(Boolean) as string[];
if (!companyNames.includes(accountCompany)) {
// 如果当前选中的公司不在列表中,清空选择
setAccountCompany('');
setAccountCompanySearch('');
}
}
}, [accountCompanyList, accountCompany]);
// 防抖:当输入值变化时,延迟 0.5 秒后更新 debouncedAddonSearch用于 API 调用)
useEffect(() => {
if (debounceTimerRef.current) {
window.clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = window.setTimeout(() => {
setDebouncedAddonSearch(addonSearchInput);
}, 500);
return () => {
if (debounceTimerRef.current) {
window.clearTimeout(debounceTimerRef.current);
}
};
}, [addonSearchInput]);
// 拉取加项列表
useEffect(() => {
if (!customerInfoLoadedRef.current && customerInfo === null) {
return;
}
const fetchList = async () => {
setAddonLoading(true);
setAddonError(null);
try {
console.log("请求数据 1", customerInfo, customerInfo?.scrm_account_id);
const selectedChannel = channelDiscounts.find(
(item) => item.discount_rate === discountRatio
);
const discountRate = selectedChannel?.discount_rate ?? null;
const res = await searchPhysicalExamAddItem({
physical_exam_id: Number(client.id),
scrm_account_id: customerInfo?.scrm_account_id || null,
scrm_account_name: customerInfo?.scrm_account_name || null,
item_name: debouncedAddonSearch.trim() || "",
discount_rate: discountRate,
});
if (res.Status === 200 && Array.isArray(res.Data)) {
const list: AddonItem[] = res.Data.map((item) => ({
id: item.item_id ? String(item.item_id) : `addon_${item.item_name}`,
name: item.item_name || '',
originalPrice:
item.original_price !== undefined && item.original_price !== null
? Number(item.original_price).toFixed(2)
: '0.00',
currentPrice:
item.actual_received_amount !== undefined && item.actual_received_amount !== null
? Number(item.actual_received_amount).toFixed(2)
: '0.00',
combinationItemCode: item.combination_item_code ?? null,
isEnjoyDiscount: item.is_enjoy_discount ?? null,
discount_name: item.discount_rate ?? null,
tags: [],
paid: false,
}));
setAddonList(list);
} else {
setAddonError(res.Message || '获取加项列表失败');
setAddonList([]);
}
} catch (err) {
console.error('获取加项列表失败', err);
setAddonError('获取加项列表失败,请稍后重试');
setAddonList([]);
} finally {
setAddonLoading(false);
}
};
fetchList();
}, [debouncedAddonSearch, customerInfo?.scrm_account_id, customerInfo?.scrm_account_name, client.id, customerInfo, channelDiscounts, discountRatio]);
const maxSelect = 15;
const selectedCount = selectedIds.size;
const filteredAddons = allAddons;
const toggleSelect = (id: string) => {
if (selectedIds.has(id)) {
setSelectedIds(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
} else {
if (selectedCount < maxSelect) {
setSelectedIds(prev => new Set(prev).add(id));
}
}
};
// 计算价格汇总
const selectedItems = allAddons.filter(item => selectedIds.has(item.id || item.name));
const totalOriginal = selectedItems.reduce((sum, item) => {
return sum + parseFloat(item.originalPrice || item.currentPrice || '0');
}, 0);
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;
// 获取标签样式
const getTagStyle = (tag: AddonTag) => {
switch (tag.type) {
case 1: // 热门
return 'bg-[#FDF0F0] text-[#BC4845]';
case 3: // 医生推荐
return 'bg-[#ECF0FF] text-[#6A6AE5]';
case 4: // 折扣信息
return 'bg-[#4C5460] text-[#F1F2F5]';
default: // 2: 普通
return 'bg-[#F1F2F5] text-[#464E5B]';
}
};
// 获取折扣信息文字(从 tags 中提取 type 4 的标签,或计算折扣)
const getDiscountText = (item: AddonItem) => {
return item.discount_name;
};
// 构建折扣选项列表带上渠道ID/名称,便于联动设置 scrm_account_id / scrm_account_name
const discountOptions = useMemo(() => {
const options: Array<{ value: number; label: string; channel_id?: string | null; channel_name?: string | null }> = [];
channelDiscounts.forEach((item) => {
const rate = typeof item.discount_rate === 'number' && item.discount_rate > 0 ? item.discount_rate : 1;
const percent = Math.round(rate * 100);
const label = item.discount_name || `${percent}%`;
options.push({
value: rate,
label,
channel_id: item.channel_id ?? null,
channel_name: item.channel_name ?? null,
});
});
return options;
}, [channelDiscounts]);
// 获取当前选中的标签
const currentDiscountLabel = useMemo(() => {
const option = discountOptions.find(opt => opt.value === discountRatio);
return option?.label;
}, [discountRatio, discountOptions]);
// 处理折扣选择,同时更新 scrm_account_id / scrm_account_name
const handleDiscountSelect = (value: number) => {
setDiscountRatio(value);
const matched = discountOptions.find(opt => opt.value === value);
if (matched && (matched.channel_id || matched.channel_name)) {
setCustomerInfo((prev) =>
prev
? {
...prev,
scrm_account_id: matched.channel_id ?? null,
scrm_account_name: matched.channel_name ?? null,
}
: prev
);
}
setIsDiscountDropdownOpen(false);
};
// 处理结算方式选择
const handlePaymentMethodSelect = (value: 'self' | 'account') => {
setPaymentMethod(value);
setIsPaymentMethodDropdownOpen(false);
};
// 处理挂账公司选择
const handleAccountCompanySelect = (value: string, label: string) => {
setAccountCompany(value);
setAccountCompanySearch(label);
setIsAccountCompanyDropdownOpen(false);
};
// 清空挂账公司
const handleClearAccountCompany = () => {
setAccountCompany('');
setAccountCompanySearch('');
setIsAccountCompanyDropdownOpen(false);
};
// 结算方式选项
const paymentMethodOptions: Array<{ value: 'self' | 'account'; label: string }> = [
{ value: 'self', label: '自费' },
{ value: 'account', label: '挂账' },
];
// 挂账公司选项(从接口获取)
const accountCompanyOptions = useMemo(() => {
if (accountCompanyList.length === 0) {
// 如果没有数据,返回默认的"圆和"
return [{ value: '圆和', label: '圆和' }];
}
return accountCompanyList.map((company) => ({
value: company.company_name || `公司${company.company_id}`,
label: company.company_name || `公司${company.company_id}`,
}));
}, [accountCompanyList]);
// 挂账公司模糊过滤
const filteredAccountCompanyOptions = useMemo(() => {
const kw = accountCompanySearch.trim().toLowerCase();
if (!kw) return accountCompanyOptions;
return accountCompanyOptions.filter((opt) => opt.label.toLowerCase().includes(kw));
}, [accountCompanyOptions, accountCompanySearch]);
// 清理轮询定时器
useEffect(() => {
return () => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
};
}, []);
// 获取自定义结算审批状态(带节流,最多每秒一次)
const fetchCustomSettlementStatus = useCallback(async () => {
const physical_exam_id = Number(client.id);
if (!physical_exam_id || !currentAddItemId) {
setCustomSettlementStatus(null);
return false;
}
// 节流如果距离上次调用不足0.8秒,则跳过
const now = Date.now();
const timeSinceLastCall = now - lastFetchStatusTimeRef.current;
if (timeSinceLastCall < 800) {
// 从 ref 获取当前状态判断是否需要轮询
return customSettlementStatus?.apply_status === 1;
}
lastFetchStatusTimeRef.current = now;
try {
const res = await getCustomSettlementApproveStatus({
physical_exam_id,
add_item_id: currentAddItemId,
});
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,
apply_reason: (res.Data as any).apply_reason,
settlement_type: (res.Data as any).settlement_type,
discount_ratio: (res.Data as any).discount_ratio,
add_item_id: currentAddItemId,
};
// 获取旧状态用于判断是否刚进入审核中
setCustomSettlementStatus(prev => {
if (status.apply_status === 1 && prev?.apply_status !== 1) {
setWaitingSeconds(0);
}
return status;
});
// 返回是否需要继续轮询1-审核中 需要轮询)
return res.Data.apply_status === 1;
} else {
setCustomSettlementStatus(null);
return false;
}
} catch (err) {
console.error('获取自定义结算审批状态失败', err);
setCustomSettlementStatus(null);
return false;
}
}, [client.id, currentAddItemId]);
// 当选中加项变化时,获取审批状态
useEffect(() => {
// 切换选中项时,先清零状态,避免错误回显旧数据
setCustomSettlementStatus(null);
setCustomSettlementType(1);
setCustomDiscountRatio(null);
setCustomFinalPrice(null);
setCustomApplyReason('');
lastFetchStatusTimeRef.current = 0; // 强制立即发起新请求
if (currentAddItemId) {
fetchCustomSettlementStatus().then((shouldPoll) => {
// 如果需要轮询(审核中),开始轮询
if (shouldPoll) {
startCustomSettlementPolling();
} else {
stopCustomSettlementPolling();
}
});
} else {
stopCustomSettlementPolling();
}
}, [currentAddItemId, client.id]); // 移除 fetchCustomSettlementStatus 依赖,避免由于状态更新导致的死循环
// 开始轮询自定义结算审批状态
const startCustomSettlementPolling = useCallback(() => {
// 清除之前的定时器
stopCustomSettlementPolling();
// 每1秒轮询一次
customSettlementPollingTimerRef.current = window.setInterval(() => {
fetchCustomSettlementStatus().then((shouldPoll) => {
// 如果不再需要轮询(审核完成),停止轮询
if (!shouldPoll) {
stopCustomSettlementPolling();
}
});
}, 1500);
}, [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;
}
const discountRatioValue = customSettlementType === 1 ? (customDiscountRatio ?? 0) : 0;
if (customSettlementType === 1 && (discountRatioValue <= 0 || discountRatioValue > 100)) {
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 * (discountRatioValue / 100);
} else {
// 自定义结算价
settlementPrice = (customFinalPrice ?? 0) / 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 * (discountRatioValue / 100)
: (customFinalPrice ?? 0);
const apply_user = localStorage.getItem('operatorName');
if (!apply_user) {
alert('请先登录');
window.location.href = '/home';
return;
}
const res = await customSettlementApply({
physical_exam_id,
listAddItemDetail,
original_settlement_price,
settlement_type: customSettlementType,
discount_ratio: customSettlementType === 1 ? discountRatioValue : undefined,
final_settlement_price: final_settlement_price ?? undefined,
apply_reason: customApplyReason.trim(),
apply_user,
});
if (res.Status === 200) {
setPaymentMessage('自定义结算申请提交成功');
setShowCustomSettlementModal(false);
// 重置表单
setCustomSettlementType(1);
setCustomDiscountRatio(100);
setCustomFinalPrice(null);
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;
}
stopCustomSettlementPolling();
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 cancel_user = localStorage.getItem('operatorName');
if (!cancel_user) {
setPaymentMessage('请先登录');
return;
}
const res = await customSettlementApplyCancel({
physical_exam_id,
add_item_id,
cancel_user,
});
if (res.Status === 200 && res.Data?.is_success === 1) {
setPaymentMessage('取消申请成功');
setCustomDiscountRatio(null);
// 停止轮询
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 {
// 调用接口获取加项PDF
const res = await getAddItemBillPdf({
exam_id: examId,
CombinationCode: combinationItemCodes,
});
if (res.Status === 200) {
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,
listAddItemCombination: Array<{
combination_item_code: string;
combination_item_price: number;
discount_rate: number;
}>,
pay_type: number,
company_id: number,
combinationItemCodes: string,
orderAmount: number
) => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
}
pollingTimerRef.current = window.setInterval(async () => {
try {
const res = await checkNativePaymentStatus({
physical_exam_id,
patient_id,
listAddItemCombination,
pay_type,
company_id,
orderAmount,
});
if (res.Status === 200) {
const result = res.Data;
console.log(result);
// 支付成功:返回 "true"
if (result === 'true') {
// 支付成功,停止轮询
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPaymentMessage('支付成功,正在生成加项单...');
setShowQrcodeModal(false);
setQrcodeUrl(null);
// 清空已选项目
setSelectedIds(new Set());
// 获取本次支付对应的加项PDF
fetchAddItemBillPdf(physical_exam_id, combinationItemCodes).then((success) => {
if (success) {
setPaymentMessage('支付成功,加项单已生成,正在跳转签署...');
// 跳转到签署页面
onGoToSign?.();
} else {
setPaymentMessage('支付成功,但加项单生成失败');
}
setTimeout(() => {
setPaymentMessage(null);
}, 3000);
});
} else if (result === 'false') {
// 支付失败,停止轮询
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
setPaymentMessage('支付失败');
setTimeout(() => {
setPaymentMessage(null);
}, 3000);
}
// 其他状态(如 "pending"、"processing" 等)继续轮询
}
} 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));
// 构建加项组合项目信息列表
const listAddItemCombination = selectedItems
.map((item) => {
const combinationItemCode = item.combinationItemCode;
if (combinationItemCode === null || combinationItemCode === undefined) {
return null;
}
let combinationItemPrice = parseFloat(item.currentPrice || item.originalPrice || '0');
// 如果自定义结算审核通过,传申请后的均价(按项平摊)
if (customSettlementStatus?.apply_status === 3 && typeof customSettlementStatus.final_settlement_price === 'number') {
combinationItemPrice = customSettlementStatus.final_settlement_price / selectedItems.length;
}
return {
combination_item_code: String(combinationItemCode),
combination_item_price: combinationItemPrice,
discount_rate: discountRatio,
};
})
.filter((item): item is { combination_item_code: string; combination_item_price: number; discount_rate: number } => item !== null);
if (listAddItemCombination.length === 0) {
setPaymentMessage('缺少加项信息,请稍后重试');
setPaymentLoading(false);
return;
}
// 获取组合项目代码(用于生成二维码接口 & 生成加项单,多个加项逗号分隔)
const combinationItemCodes = listAddItemCombination
.map((item) => item.combination_item_code)
.join(',');
// 获取 patient_id必须从接口返回的客户信息中获取
if (!customerInfo?.patient_id) {
setPaymentMessage('缺少患者ID请稍后重试');
setPaymentLoading(false);
return;
}
const patient_id = customerInfo.patient_id;
if (paymentMethod === 'self') {
// 自费模式:如果是 0 元支付,直接查询状态,不生成二维码
if (totalCurrent === 0) {
startPaymentPolling(
physical_exam_id,
patient_id,
listAddItemCombination,
12, // 微信支付
0, // 自费模式company_id 传 0
combinationItemCodes,
0
);
return;
}
// 自费模式:生成二维码
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,
listAddItemCombination,
12, // 微信支付
0, // 自费模式company_id 传 0
combinationItemCodes,
totalCurrent
);
} else {
setPaymentMessage(res.Message || '生成支付二维码失败');
}
} else {
// 挂账模式:直接查询
// 验证是否选择了挂账公司
console.log('accountCompany', accountCompany);
if (!accountCompany || accountCompany.trim() === '') {
setPaymentMessage('请选择挂账公司');
setPaymentLoading(false);
return;
}
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,
listAddItemCombination,
pay_type: 13, // 挂账公司
company_id: selectedCompany.company_id,
orderAmount: totalCurrent,
});
if (res.Status === 200) {
const result = res.Data;
if (result === 'true') {
setPaymentMessage('挂账成功,正在生成加项单...');
setSelectedIds(new Set());
// 获取本次挂账对应的加项PDF
fetchAddItemBillPdf(physical_exam_id, combinationItemCodes).then((success) => {
if (success) {
setPaymentMessage('挂账成功,加项单已生成,正在跳转签署...');
onGoToSign?.();
} 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);
}
};
const isApprovedOrRejected =
(customSettlementStatus?.apply_status === 3 || customSettlementStatus?.apply_status === 4) &&
customSettlementStatus?.add_item_id === currentAddItemId;
return (
<div className='space-y-4'>
{/* 标题和说明 */}
<div>
<div className='flex items-center justify-between mb-3 gap-4'>
<div className='flex items-center gap-2'>
<h3 className='text-lg font-semibold text-gray-900 whitespace-nowrap'></h3>
<div className='flex items-center gap-2 text-xs text-gray-600'>
{/* <span className='whitespace-nowrap'>折扣比例</span> */}
<div ref={discountDropdownRef} className='relative'>
<button
type='button'
onClick={() => setIsDiscountDropdownOpen(!isDiscountDropdownOpen)}
className={cls(
'border-gray-300 rounded-lg px-3 py-1.5 text-xs text-gray-700 bg-white',
'focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500',
'min-w-[140px] flex items-center justify-between gap-2',
'hover:border-gray-400 transition-colors'
)}
>
<span>{currentDiscountLabel}</span>
<span className={cls('text-gray-400 transition-transform text-[10px]', isDiscountDropdownOpen && 'rotate-180')}>
</span>
</button>
{isDiscountDropdownOpen && (
<div className='absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg overflow-hidden'>
{discountOptions.map((option) => (
<button
key={option.value}
type='button'
onClick={() => handleDiscountSelect(option.value)}
className={cls(
'w-full px-3 py-2 text-xs text-left transition-colors',
'hover:bg-gray-50',
discountRatio === option.value && 'bg-blue-50 text-blue-700 font-medium',
discountRatio !== option.value && 'text-gray-700'
)}
>
{option.label}
</button>
))}
</div>
)}
</div>
</div>
</div>
<div className='w-[260px] flex items-center gap-2'>
<Input
placeholder='搜索 加项名称'
value={addonSearchInput}
onChange={(e) => setAddonSearchInput(e.target.value)}
className='text-sm flex-1'
/>
{addonSearchInput && (
<Button
className='px-3 py-1.5 text-xs whitespace-nowrap'
onClick={() => setAddonSearchInput('')}
>
</Button>
)}
</div>
</div>
{/* <div className='text-xs text-gray-600 space-y-1'>
<div>最多可选 {maxSelect} 项 · 一排 5 个</div>
<div>已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。</div>
</div> */}
</div>
{/* 加项网格 */}
<div className='overflow-y-auto overflow-x-hidden max-h-[366px]'>
<div className='grid grid-cols-5 gap-3 min-h-[142px]'>
{addonError && (
<div className='col-span-5 text-xs text-amber-600'>{addonError}</div>
)}
{addonLoading && (
<div className='col-span-5 text-xs text-gray-500'>...</div>
)}
{!addonLoading && !addonError && filteredAddons.length === 0 && (
<div className='col-span-5 text-xs text-gray-500'></div>
)}
{filteredAddons.map((item) => {
const id = item.id || item.name;
const isSelected = selectedIds.has(id);
const origPrice = parseFloat(item.originalPrice || '0');
const currPrice = parseFloat(item.currentPrice || '0');
return (
<div
key={id}
className='border rounded-lg p-3 cursor-pointer transition-all flex flex-col relative'
onClick={() => toggleSelect(id)}
>
{/* 无折扣标签图片 - 浮动在右上角(当 is_enjoy_discount 为 0 或 null 时显示) */}
{(!item.isEnjoyDiscount || item.isEnjoyDiscount === 0) && (
<img
src={nozImage}
alt='无折扣'
className='absolute top-[-10px] right-[-10px] w-12 h-12 object-contain pointer-events-none z-10'
/>
)}
{/* 第一行:复选框 + 名称 */}
<div className='flex items-start gap-2 mb-1'>
<input
type='checkbox'
checked={isSelected}
onChange={() => toggleSelect(id)}
onClick={(e) => e.stopPropagation()}
className='mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500'
/>
<div className='flex-1 min-w-0'>
<div className='font-semibold text-[14px] text-gray-900'>{item.name}</div>
</div>
</div>
{/* 第二行:标签 */}
{item.tags && item.tags.length >= 0 && (
<div className='flex flex-wrap gap-1 mb-1 pl-6'>
{item.tags
.filter(t => t.type !== 4) // 折扣信息单独显示
.map((tag, idx) => (
<span
key={idx}
className={`text-[10px] px-2 rounded-full ${getTagStyle(tag)}`}
>
{tag.title}
</span>
))}
</div>
)}
{/* 第三行:价格信息(固定在卡片底部) */}
<div className='mt-auto pt-1'>
<div className='flex flex-col'>
{origPrice >= 0 && origPrice >= currPrice && (
<span className='text-xs text-gray-400 line-through'>¥{origPrice.toFixed(2)}</span>
)}
<span className='text-[13px] font-bold text-[#447955]'>{getDiscountText(item)}</span>
<span className='text-[13px] font-bold text-red-600'>¥{currPrice.toFixed(2)}</span>
{/*
<div className='flex items-center justify-between gap-2'>
<span></span>
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
{getDiscountText(item)}
</span>
</div> */}
</div>
</div>
</div>
);
})}
</div>
</div>
{/* 底部汇总和支付 */}
<div className='border-t pt-4 mt-6 flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='space-y-1 text-sm'>
<div className='text-gray-600'>
: <span className='text-gray-900'>¥{totalOriginal.toFixed(2)}</span>
</div>
<div className='text-gray-600'>
: <span className='font-bold text-red-600'>¥{discount.toFixed(2)}</span>
</div>
<div className='text-gray-600'>
: <span className='font-bold text-red-600'>¥{(totalCurrent).toFixed(2)}</span>
</div>
{/* <div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</div> */}
</div>
{/* 添加自定义结算 */}
{localStorage.getItem('authCode')?.includes('HisTijian_Btn_JiesuanShenqing') && selectedItems.length > 0 && (
<div className='flex flex-col items-end gap-2 relative'>
<Button
onClick={() => setShowCustomSettlementModal(true)}
className='px-3 py-1.5 text-xs text-white bg-blue-600'
>
{!customSettlementStatus ||
!customSettlementStatus.apply_status ||
customSettlementStatus.apply_status === 1 ||
customSettlementStatus.add_item_id !== currentAddItemId
? '申请自定义结算'
: customSettlementStatus.apply_status === 3
? '自定义结算(已通过)'
: '自定义结算(未通过)'}
</Button>
</div>
)}
</div>
{paymentMessage && (
<div className={`text-sm text-center mt-2 ${paymentMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
</div>
)}
<div className='flex items-center gap-3'>
{/* 结算方式 */}
<div className='flex items-center gap-2 text-xs text-gray-600'>
<span className='whitespace-nowrap'></span>
<div ref={paymentMethodDropdownRef} className='relative'>
<button
type='button'
onClick={() => setIsPaymentMethodDropdownOpen(!isPaymentMethodDropdownOpen)}
className={cls(
'border border-gray-300 rounded-lg px-3 py-1.5 text-xs text-gray-700 bg-white',
'focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500',
'min-w-[100px] flex items-center justify-between gap-2',
'hover:border-gray-400 transition-colors'
)}
>
<span>{paymentMethodOptions.find(opt => opt.value === paymentMethod)?.label || '自费'}</span>
<span className={cls('text-gray-400 transition-transform text-[10px]', isPaymentMethodDropdownOpen && 'rotate-180')}>
</span>
</button>
{isPaymentMethodDropdownOpen && (
<div className='absolute z-50 w-full bottom-full mb-1 bg-white border border-gray-300 rounded-lg shadow-lg overflow-hidden'>
{paymentMethodOptions.map((option) => (
<button
key={option.value}
type='button'
onClick={() => handlePaymentMethodSelect(option.value)}
className={cls(
'w-full px-3 py-2 text-xs text-left transition-colors',
'hover:bg-gray-50',
paymentMethod === option.value && 'bg-blue-50 text-blue-700 font-medium',
paymentMethod !== option.value && 'text-gray-700'
)}
>
{option.label}
</button>
))}
</div>
)}
</div>
</div>
{/* 挂账公司 */}
{paymentMethod === 'account' && (
<div className='flex items-center gap-2 text-xs text-gray-600'>
<span className='whitespace-nowrap'></span>
<div ref={accountCompanyDropdownRef} className='relative'>
<div
className={cls(
'border border-gray-300 rounded-lg px-3 py-1.5 text-xs text-gray-700 bg-white',
'focus-within:ring-1 focus-within:ring-blue-500 focus-within:border-blue-500',
'min-w-[200px] flex items-center justify-between gap-2',
'hover:border-gray-400 transition-colors cursor-text'
)}
onClick={() => setIsAccountCompanyDropdownOpen(true)}
>
<input
type='text'
value={accountCompanySearch}
onChange={(e) => {
setAccountCompanySearch(e.target.value);
setIsAccountCompanyDropdownOpen(true);
}}
placeholder={'请输入公司名称'}
className='w-full outline-none text-xs bg-transparent'
onClick={(e) => e.stopPropagation()}
/>
<div className='flex items-center gap-1'>
{accountCompanySearch && (
<button
type='button'
onClick={(e) => {
e.stopPropagation();
handleClearAccountCompany();
}}
className='text-gray-400 hover:text-gray-600 transition-colors text-sm leading-none w-4 h-4 flex items-center justify-center'
>
×
</button>
)}
<span className={cls('text-gray-400 transition-transform text-[10px]', isAccountCompanyDropdownOpen && 'rotate-180')}>
</span>
</div>
</div>
{isAccountCompanyDropdownOpen && (
<div className='absolute z-50 w-full bottom-full mb-1 bg-white border border-gray-300 rounded-lg shadow-lg overflow-hidden max-h-[320px] overflow-y-auto'>
{filteredAccountCompanyOptions.length === 0 && (
<div className='px-3 py-2 text-xs text-gray-400'></div>
)}
{filteredAccountCompanyOptions.map((option, idx) => (
<button
key={`${option.value}-${idx}`}
type='button'
onClick={() => handleAccountCompanySelect(option.value, option.label)}
className={cls(
'w-full px-3 py-2 text-xs text-left transition-colors',
'hover:bg-gray-50',
accountCompany === option.value && 'bg-blue-50 text-blue-700 font-medium',
accountCompany !== option.value && 'text-gray-700'
)}
>
{option.label}
</button>
))}
</div>
)}
</div>
</div>
)}
<Button
className='bg-blue-600 rounded-3xl text-white px-6 py-3 text-base font-medium'
disabled={selectedCount === 0 || paymentLoading}
onClick={handlePayment}
>
{paymentLoading ? '处理中...' : `确认支付 ¥${totalCurrent.toFixed(2)}`}
</Button>
</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>
)
}
{/* 自定义结算弹窗 */}
{
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'
disabled={isApprovedOrRejected}
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',
isApprovedOrRejected && 'opacity-70 cursor-not-allowed'
)}
>
</button>
<button
type='button'
disabled={isApprovedOrRejected}
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',
isApprovedOrRejected && 'opacity-70 cursor-not-allowed'
)}
>
</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 ?? 0) / 10).toFixed(1)}
</span>
</div>
<div className='flex items-center gap-2'>
<Input
type='number'
min='0'
max='100'
step='1'
disabled={isApprovedOrRejected}
value={customDiscountRatio ?? ''}
onChange={(e) => {
const raw = e.target.value;
if (raw === '') {
setCustomDiscountRatio(null);
return;
}
const val = Number(raw);
if (Number.isNaN(val)) return;
if (val < 1) {
setCustomDiscountRatio(1);
} else if (val > 100) {
setCustomDiscountRatio(100);
} else {
setCustomDiscountRatio(val);
}
}}
className='w-28 text-sm'
/>
<span className='text-xs text-gray-500'>1-100100 </span>
</div>
<div className='text-xs text-gray-500'>
: <span className='font-semibold text-red-600'>¥{(totalCurrent * ((customDiscountRatio ?? 0) / 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'
disabled={isApprovedOrRejected}
value={customFinalPrice ?? ''}
onChange={(e) => {
const rawValue = e.target.value;
if (rawValue === '') {
setCustomFinalPrice(null);
return;
}
const val = Number(rawValue);
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}
disabled={isApprovedOrRejected}
onChange={(e) => setCustomApplyReason(e.target.value)}
placeholder='请输入申请理由'
rows={3}
className={cls(
'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',
isApprovedOrRejected && 'bg-gray-50 opacity-70 cursor-not-allowed'
)}
/>
</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'
>
{isApprovedOrRejected ? '关闭' : '取消'}
</Button>
{!isApprovedOrRejected && (
<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 >
);
};