1633 lines
64 KiB
TypeScript
1633 lines
64 KiB
TypeScript
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 type { InputAddItemCombinationInfo, OutputCustomSettlementApplyApproveItem } from '../../api/types';
|
||
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;
|
||
discount_ratio?: number | 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;
|
||
listAddItemCombination?: OutputCustomSettlementApplyApproveItem[] | 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,
|
||
discount_ratio: item.discount_ratio ?? 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,
|
||
listAddItemCombination: res.Data.listAddItemCombination,
|
||
};
|
||
|
||
// 获取旧状态用于判断是否刚进入审核中
|
||
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');
|
||
|
||
// if (customSettlementType === 1) {
|
||
// // 按比例折扣
|
||
// settlementPrice = originalPrice * (discountRatioValue / 100);
|
||
// } else {
|
||
// // 自定义结算价
|
||
// settlementPrice = (customFinalPrice ?? 0) / selectedItems.length; // 平均分配
|
||
// }
|
||
|
||
const discount_ratio = (() => {
|
||
const r = item.discount_ratio;
|
||
if (r == null || typeof r !== 'number') return 0;
|
||
return r > 1 ? r : Math.round(r * 100);
|
||
})();
|
||
|
||
return {
|
||
combination_item_code: String(combinationItemCode),
|
||
combination_item_name: item.name,
|
||
original_price: originalPrice,
|
||
settlement_price: originalPrice * (discount_ratio / 100),
|
||
discount_ratio: discount_ratio,
|
||
};
|
||
})
|
||
.filter((item): item is { combination_item_code: string; combination_item_name: string; original_price: number; settlement_price: number; discount_ratio: number } => item !== null);
|
||
|
||
const original_settlement_price = listAddItemDetail.reduce((sum, item) => sum + item.original_price, 0);
|
||
|
||
const final_settlement_price = customSettlementType === 1
|
||
? totalCurrent * ((customDiscountRatio ?? 0) / 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: InputAddItemCombinationInfo[],
|
||
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));
|
||
|
||
// 构建加项组合项目信息列表
|
||
let listAddItemCombination: InputAddItemCombinationInfo[] = [];
|
||
|
||
// 如果自定义结算审核通过,直接使用接口返回的项目列表
|
||
if (customSettlementStatus?.apply_status === 3 && customSettlementStatus.listAddItemCombination) {
|
||
listAddItemCombination = customSettlementStatus.listAddItemCombination.map((item: OutputCustomSettlementApplyApproveItem) => ({
|
||
combination_item_code: item.combination_item_code,
|
||
combination_item_price: item.combination_item_price,
|
||
discount_rate: item.discount_rate
|
||
}));
|
||
} else {
|
||
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,
|
||
} as InputAddItemCombinationInfo;
|
||
})
|
||
.filter((item): item is InputAddItemCombinationInfo => item !== null);
|
||
}
|
||
|
||
if (listAddItemCombination.length === 0) {
|
||
setPaymentMessage('缺少加项信息,请稍后重试');
|
||
setPaymentLoading(false);
|
||
return;
|
||
}
|
||
|
||
// 获取组合项目代码(用于生成二维码接口 & 生成加项单,多个加项逗号分隔)
|
||
const combinationItemCodes = listAddItemCombination
|
||
.map((item) => item.combination_item_code)
|
||
.filter(Boolean)
|
||
.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 是微信支付 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,
|
||
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-[clamp(248px,calc(100vh-520px),366px)]'>
|
||
<div className='grid grid-cols-5 gap-2'>
|
||
{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 h-[120px] overflow-hidden'
|
||
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-100,100 表示不打折</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 >
|
||
);
|
||
};
|
||
|
||
|