Files
ipad/src/components/exam/ExamAddonPanel.tsx
2026-01-07 16:10:15 +08:00

896 lines
34 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 } from 'react';
import type { ExamClient } from '../../data/mockData';
import { searchPhysicalExamAddItem, getAddItemCustomerInfo, getChannelCompanyList, createNativePaymentQrcode, checkNativePaymentStatus, getAddItemBillPdf } 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;
}
export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
const [addonList, setAddonList] = useState<AddonItem[]>([]);
// 防抖:内部输入值(用于显示)
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 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;
} | 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(() => {
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) {
// 保存客户信息
if (res.Data?.customerInfo) {
setCustomerInfo({
patient_id: res.Data.customerInfo.patient_id,
customer_name: res.Data.customerInfo.customer_name,
phone: res.Data.customerInfo.phone,
});
}
// 保存渠道折扣信息
if (res.Data?.listChannelDiscount && res.Data.listChannelDiscount.length > 0) {
setChannelDiscounts(res.Data.listChannelDiscount);
const rate = res.Data.listChannelDiscount[0]?.discount_rate;
if (typeof rate === 'number' && rate > 0) {
setDiscountRatio(rate);
}
} else {
setChannelDiscounts([]);
}
} else {
setChannelDiscounts([]);
}
} catch (err) {
console.error('获取加项客户信息失败', err);
}
};
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);
// 设置默认值为第一个公司
const firstCompany = res.Data[0];
if (firstCompany?.company_name) {
setAccountCompany(firstCompany.company_name);
}
} else {
setAccountCompanyList([]);
}
} catch (err) {
console.error('获取挂账公司列表失败', err);
setAccountCompanyList([]);
}
};
fetchCompanyList();
}, []);
// 当挂账公司列表更新时,确保当前选中的公司在列表中
useEffect(() => {
if (accountCompanyList.length > 0) {
const companyNames = accountCompanyList.map((c) => c.company_name).filter(Boolean) as string[];
if (!companyNames.includes(accountCompany)) {
// 如果当前选中的公司不在列表中,更新为第一个公司
const firstCompany = accountCompanyList[0];
if (firstCompany?.company_name) {
setAccountCompany(firstCompany.company_name);
}
}
}
}, [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(() => {
const fetchList = async () => {
setAddonLoading(true);
setAddonError(null);
try {
const res = await searchPhysicalExamAddItem({
physical_exam_id: Number(client.id),
discount_ratio: discountRatio || 1,
item_name: debouncedAddonSearch.trim() || "",
});
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)
: item.original_price !== undefined && item.original_price !== null
? Number(item.original_price).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, discountRatio]);
const allAddons = useMemo(() => addonList, [addonList]);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
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 totalCurrent = selectedItems.reduce((sum, item) => {
return sum + parseFloat(item.currentPrice || item.originalPrice || '0');
}, 0);
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;
};
// 构建折扣选项列表
const discountOptions = useMemo(() => {
const options: Array<{ value: number; label: string }> = [];
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 });
});
return options;
}, [channelDiscounts]);
// 获取当前选中的标签
const currentDiscountLabel = useMemo(() => {
const option = discountOptions.find(opt => opt.value === discountRatio);
return option?.label;
}, [discountRatio, discountOptions]);
// 处理折扣选择
const handleDiscountSelect = (value: number) => {
setDiscountRatio(value);
setIsDiscountDropdownOpen(false);
};
// 处理结算方式选择
const handlePaymentMethodSelect = (value: 'self' | 'account') => {
setPaymentMethod(value);
setIsPaymentMethodDropdownOpen(false);
};
// 处理挂账公司选择
const handleAccountCompanySelect = (value: string) => {
setAccountCompany(value);
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]);
// 清理轮询定时器
useEffect(() => {
return () => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
};
}, []);
// 获取加项PDF按本次支付的组合代码生成对应的加项单
const fetchAddItemBillPdf = async (examId: number, combinationItemCodes: string) => {
try {
// 调用接口获取加项PDF
const res = await getAddItemBillPdf({
exam_id: examId,
CombinationCode: combinationItemCodes,
});
if (res.Status === 200 && res.Data?.pdf_url && res.Data?.pdf_sort !== undefined && res.Data?.pdf_sort !== null) {
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
) => {
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,
});
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('支付成功,加项单已生成');
} 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;
}
const combinationItemPrice = parseFloat(item.currentPrice || item.originalPrice || '0');
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') {
// 自费模式:生成二维码
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
);
} 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,
listAddItemCombination,
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, combinationItemCodes).then((success) => {
if (success) {
setPaymentMessage('挂账成功,加项单已生成');
} else {
setPaymentMessage('挂账成功,但加项单生成失败');
}
setTimeout(() => {
setPaymentMessage(null);
}, 3000);
});
} else {
setPaymentMessage('挂账失败');
setTimeout(() => {
setPaymentMessage(null);
}, 3000);
}
} else {
setPaymentMessage(res.Message || '挂账失败');
}
}
} catch (err) {
console.error('支付处理失败', err);
setPaymentMessage('支付处理失败,请稍后重试');
} finally {
setPaymentLoading(false);
}
};
return (
<div className='space-y-4'>
{/* 标题和说明 */}
<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(0)}</span>
)}
<div className='flex items-center justify-between gap-2'>
<span className='text-[14px] font-bold text-red-600'>¥{currPrice.toFixed(2)}</span>
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
{getDiscountText(item)}
</span>
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* 底部汇总和支付 */}
<div className='border-t pt-4 mt-6 flex items-center justify-between'>
<div className='space-y-1 text-sm'>
<div className='text-gray-600'>
: <span className='text-gray-900'>¥{totalOriginal.toFixed(2)}</span>
</div>
<div className='text-gray-600'>
: <span className='text-xl font-bold text-red-600'>¥{totalCurrent.toFixed(2)}</span>
{discount > 0 && (
<span className='text-gray-500 ml-1'> ¥{discount.toFixed(2)}</span>
)}
</div>
{/* <div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</div> */}
</div>
{paymentMessage && (
<div className={`text-sm text-center mt-2 ${paymentMessage.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
{paymentMessage}
</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'>
<button
type='button'
onClick={() => setIsAccountCompanyDropdownOpen(!isAccountCompanyDropdownOpen)}
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>{accountCompany}</span>
<span className={cls('text-gray-400 transition-transform text-[10px]', isAccountCompanyDropdownOpen && 'rotate-180')}>
</span>
</button>
{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-[500px] overflow-y-auto'>
{accountCompanyOptions.map((option) => (
<button
key={option.value}
type='button'
onClick={() => handleAccountCompanySelect(option.value)}
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-[#269745] hover:bg-[#269745]/80 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>
)}
</div>
);
};