diff --git a/src/components/exam/ExamAddonPanel.tsx b/src/components/exam/ExamAddonPanel.tsx new file mode 100644 index 0000000..e8340b1 --- /dev/null +++ b/src/components/exam/ExamAddonPanel.tsx @@ -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>( + 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 ( +
+ {/* 标题和说明 */} +
+
+

体检套餐加项选择

+
+ setAddonSearch(e.target.value)} + className='text-sm' + /> +
+
+
+
最多可选 {maxSelect} 项 · 一排 5 个
+
已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。
+
+
+ + {/* 加项网格 */} +
+ {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 ( +
toggleSelect(id)} + > + {/* 复选框 */} +
+ 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' + /> +
+ {/* 项目名称 */} +
{item.name}
+ + {/* 标签 */} + {item.tags && item.tags.length > 0 && ( +
+ {item.tags + .filter(t => t.type !== 4) // 折扣信息单独显示 + .map((tag, idx) => ( + + {tag.title} + + ))} +
+ )} +
+
+ + {/* 价格信息 */} +
+
+ {origPrice > 0 && origPrice > currPrice && ( + ¥{origPrice.toFixed(0)} + )} +
+ ¥{currPrice.toFixed(0)} + {getDiscountText(item)} +
+
+
+
+ ); + })} +
+ + {/* 底部汇总和支付 */} +
+
+
+ 加项原价合计: ¥{totalOriginal.toFixed(0)} +
+
+ 渠道折扣价: ¥{totalCurrent.toFixed(0)} + {discount > 0 && ( + 已优惠 ¥{discount.toFixed(0)} + )} +
+
结算方式: 个人支付 (微信 / 支付宝)
+
+ +
+
+ ); +}; + + diff --git a/src/components/exam/ExamModal.tsx b/src/components/exam/ExamModal.tsx index f749fe0..c237ed3 100644 --- a/src/components/exam/ExamModal.tsx +++ b/src/components/exam/ExamModal.tsx @@ -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>( - 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 ( -
- {/* 标题和说明 */} -
-
-

体检套餐加项选择

-
- setAddonSearch(e.target.value)} - className='text-sm' - /> -
-
-
-
最多可选 {maxSelect} 项 · 一排 5 个
-
已勾选 {selectedCount} 项,自费加项费用按渠道折扣价结算。
-
-
- - {/* 加项网格 */} -
- {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 ( -
toggleSelect(id)} - > - {/* 复选框 */} -
- 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' - /> -
- {/* 项目名称 */} -
{item.name}
- - {/* 标签 */} - {item.tags && item.tags.length > 0 && ( -
- {item.tags - .filter(t => t.type !== 4) // 折扣信息单独显示 - .map((tag, idx) => ( - - {tag.title} - - ))} -
- )} -
-
- - {/* 价格信息 */} -
-
- {origPrice > 0 && origPrice > currPrice && ( - ¥{origPrice.toFixed(0)} - )} -
- ¥{currPrice.toFixed(0)} - {getDiscountText(item)} -
-
-
-
- ); - })} -
- - {/* 底部汇总和支付 */} -
-
-
- 加项原价合计: ¥{totalOriginal.toFixed(0)} -
-
- 渠道折扣价: ¥{totalCurrent.toFixed(0)} - {discount > 0 && ( - 已优惠 ¥{discount.toFixed(0)} - )} -
-
结算方式: 个人支付 (微信 / 支付宝)
-
- -
-
- ); -}; - const ExamDeliveryPanel = ({ client }: { client: ExamClient }) => (