254 lines
9.0 KiB
TypeScript
254 lines
9.0 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
import type { ExamClient } from '../../data/mockData';
|
|
import { searchPhysicalExamAddItem } from '../../api';
|
|
import { Button, Input } from '../ui';
|
|
|
|
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;
|
|
}
|
|
|
|
interface ExamAddonPanelProps {
|
|
client: ExamClient;
|
|
}
|
|
|
|
export const ExamAddonPanel = ({ client }: ExamAddonPanelProps) => {
|
|
const [addonList, setAddonList] = useState<AddonItem[]>([]);
|
|
const [addonSearch, setAddonSearch] = useState('');
|
|
const [addonLoading, setAddonLoading] = useState(false);
|
|
const [addonError, setAddonError] = useState<string | null>(null);
|
|
|
|
// 拉取加项列表
|
|
useEffect(() => {
|
|
const physical_exam_id = Number(client.id);
|
|
if (!physical_exam_id) {
|
|
setAddonError('缺少体检ID');
|
|
return;
|
|
}
|
|
|
|
const fetchList = async () => {
|
|
setAddonLoading(true);
|
|
setAddonError(null);
|
|
try {
|
|
const res = await searchPhysicalExamAddItem({
|
|
physical_exam_id,
|
|
item_name: addonSearch.trim() || null,
|
|
});
|
|
if (res.Status === 200 && res.Data?.addItemList) {
|
|
const list: AddonItem[] = res.Data.addItemList.map((item) => ({
|
|
id: item.item_id ? String(item.item_id) : `addon_${item.item_name}`,
|
|
name: item.item_name || '',
|
|
originalPrice: item.original_price !== undefined ? Number(item.original_price).toFixed(2) : '0.00',
|
|
currentPrice: item.actual_received_amount !== undefined
|
|
? Number(item.actual_received_amount).toFixed(2)
|
|
: (item.original_price !== undefined ? Number(item.original_price).toFixed(2) : '0.00'),
|
|
tags: [],
|
|
paid: false,
|
|
}));
|
|
setAddonList(list);
|
|
} else {
|
|
setAddonError(res.Message || '获取加项列表失败');
|
|
setAddonList([]);
|
|
}
|
|
} catch (err) {
|
|
console.error('获取加项列表失败', err);
|
|
setAddonError('获取加项列表失败,请稍后重试');
|
|
setAddonList([]);
|
|
} finally {
|
|
setAddonLoading(false);
|
|
}
|
|
};
|
|
fetchList();
|
|
}, [addonSearch, client.id]);
|
|
|
|
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) => {
|
|
const discountTag = item.tags?.find(t => t.type === 4);
|
|
if (discountTag) return discountTag.title;
|
|
|
|
const orig = parseFloat(item.originalPrice || '0');
|
|
const curr = parseFloat(item.currentPrice || '0');
|
|
if (orig > 0 && curr < orig) {
|
|
const percent = Math.round((curr / orig) * 100);
|
|
return `渠道 ${percent} 折`;
|
|
}
|
|
return '渠道价';
|
|
};
|
|
|
|
return (
|
|
<div className='space-y-4'>
|
|
{/* 标题和说明 */}
|
|
<div>
|
|
<div className='flex items-center justify-between mb-2 gap-3'>
|
|
<h3 className='text-lg font-semibold text-gray-900'>体检套餐加项选择</h3>
|
|
<div className='w-[260px]'>
|
|
<Input
|
|
placeholder='搜索 加项名称'
|
|
value={addonSearch}
|
|
onChange={(e) => setAddonSearch(e.target.value)}
|
|
className='text-sm'
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className='text-xs text-gray-600 space-y-1'>
|
|
<div>最多可选 {maxSelect} 项 · 一排 5 个</div>
|
|
<div>已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 加项网格 */}
|
|
<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'
|
|
onClick={() => toggleSelect(id)}
|
|
>
|
|
{/* 第一行:复选框 + 名称 */}
|
|
<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-2 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-2'>
|
|
<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(0)}</span>
|
|
<span className={`text-[10px] px-2 rounded-full bg-[#EAFCF1] text-[#447955] whitespace-nowrap`}>
|
|
{getDiscountText(item)}
|
|
</span>
|
|
</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(0)}</span>
|
|
</div>
|
|
<div className='text-gray-600'>
|
|
渠道折扣价: <span className='text-xl font-bold text-red-600'>¥{totalCurrent.toFixed(0)}</span>
|
|
{discount > 0 && (
|
|
<span className='text-gray-500 ml-1'>已优惠 ¥{discount.toFixed(0)}</span>
|
|
)}
|
|
</div>
|
|
<div className='text-xs text-gray-500'>结算方式: 个人支付 (微信 / 支付宝)</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}
|
|
>
|
|
确认支付 ¥{totalCurrent.toFixed(0)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|