分离体检加项面板

This commit is contained in:
xianyi
2025-12-15 15:43:17 +08:00
parent d70dc3cf3d
commit f4f218dec3
2 changed files with 215 additions and 208 deletions

View File

@@ -0,0 +1,214 @@
import { useState } from 'react';
import type { ExamClient } from '../../data/mockData';
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;
price?: number; // 兼容 addonOptions 结构
}
export const ExamAddonPanel = ({ client }: { client: ExamClient }) => {
// 从 client 获取加项选项数据
const addonOptions = (client['addonOptions' as keyof ExamClient] as AddonItem[] | undefined) || [];
const addonSummary = (client['addonSummary' as keyof ExamClient] as AddonItem[] | undefined) || [];
// 合并数据,优先使用 addonOptions如果没有则使用 addonSummary
const allAddons: AddonItem[] = addonOptions.length > 0
? addonOptions.map(item => ({
id: item.id || `addon_${item.name}`,
name: item.name,
paid: item.paid || false,
tags: item.tags || [],
originalPrice: item.originalPrice || (item.price ? item.price.toFixed(2) : '0.00'),
currentPrice: item.currentPrice || (item.price ? item.price.toFixed(2) : '0.00'),
}))
: addonSummary;
const [selectedIds, setSelectedIds] = useState<Set<string>>(
new Set(allAddons.filter(item => item.paid).map(item => item.id || item.name))
);
const maxSelect = 15;
const selectedCount = selectedIds.size;
const [addonSearch, setAddonSearch] = useState('');
const filteredAddons = addonSearch.trim()
? allAddons.filter(item =>
item.name.toLowerCase().includes(addonSearch.toLowerCase())
)
: 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]'>
{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'
}`}
onClick={() => toggleSelect(id)}
>
{/* 复选框 */}
<div className='flex items-start gap-2 mb-2'>
<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 mb-1'>{item.name}</div>
{/* 标签 */}
{item.tags && item.tags.length > 0 && (
<div className='flex flex-wrap gap-1 mb-2'>
{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>
</div>
{/* 价格信息 */}
<div className='mt-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>
);
};

View File

@@ -5,6 +5,7 @@ import type { CustomerAppointmentInfo, CustomerExamAddItem, CustomerInfo, Output
import { getCustomerDetail, getPhysicalExamProgressDetail, getTongyishuPdf, signInMedicalExamCenter, submitTongyishuSign } from '../../api';
import { Button, Input, SignaturePad, type SignaturePadHandle } from '../ui';
import { ExamDetailPanel } from './ExamDetailPanel';
import { ExamAddonPanel } from './ExamAddonPanel';
interface ExamModalProps {
client: ExamClient;
@@ -558,214 +559,6 @@ const ExamSignPanel = ({ examId }: { examId?: number }) => {
);
};
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;
price?: number; // 兼容 addonOptions 结构
}
const ExamAddonPanel = ({ client }: { client: ExamClient }) => {
// 从 client 获取加项选项数据
const addonOptions = (client['addonOptions' as keyof ExamClient] as AddonItem[] | undefined) || [];
const addonSummary = (client['addonSummary' as keyof ExamClient] as AddonItem[] | undefined) || [];
// 合并数据,优先使用 addonOptions如果没有则使用 addonSummary
const allAddons: AddonItem[] = addonOptions.length > 0
? addonOptions.map(item => ({
id: item.id || `addon_${item.name}`,
name: item.name,
paid: item.paid || false,
tags: item.tags || [],
originalPrice: item.originalPrice || (item.price ? item.price.toFixed(2) : '0.00'),
currentPrice: item.currentPrice || (item.price ? item.price.toFixed(2) : '0.00'),
}))
: addonSummary;
const [selectedIds, setSelectedIds] = useState<Set<string>>(
new Set(allAddons.filter(item => item.paid).map(item => item.id || item.name))
);
const maxSelect = 15;
const selectedCount = selectedIds.size;
const [addonSearch, setAddonSearch] = useState('');
const filteredAddons = addonSearch.trim()
? allAddons.filter(item =>
item.name.toLowerCase().includes(addonSearch.toLowerCase())
)
: 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]'>
{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'
}`}
onClick={() => toggleSelect(id)}
>
{/* 复选框 */}
<div className='flex items-start gap-2 mb-2'>
<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 mb-1'>{item.name}</div>
{/* 标签 */}
{item.tags && item.tags.length > 0 && (
<div className='flex flex-wrap gap-1 mb-2'>
{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>
</div>
{/* 价格信息 */}
<div className='mt-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>
);
};
const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => (
<div className='flex justify-center'>
<div className='w-full rounded-2xl border shadow-sm px-6 py-4 text-xs text-gray-800'>