加项样式

This commit is contained in:
YI FANG
2025-12-01 10:25:27 +08:00
parent 64cb6b6ab0
commit 63a935fc25
4 changed files with 854 additions and 58 deletions

View File

@@ -30,49 +30,79 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
};
return (
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/30'>
<div className='w-[720px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'>
<div className='px-4 py-3 border-b flex items-center justify-between'>
<div className='flex items-center gap-3'>
<span className='font-semibold'>{client.name}</span>
<span className='text-xs text-gray-500'>{client.id}</span>
<Badge>{client.level}</Badge>
<div className='fixed inset-0 z-40 flex items-center justify-center bg-black/50'>
<div className='w-[960px] max-w-[95vw] bg-white rounded-2xl shadow-xl overflow-hidden text-sm'>
{/* Header 区域:增加了内边距 padding */}
<div className='px-8 pt-6 pb-2'>
<div className='flex items-center justify-between'>
{/* 左侧:姓名 + VIP + 体检号 */}
<div className='flex items-end gap-3'>
<span className='text-2xl font-bold text-gray-900 leading-none'>
{client.name}
</span>
{/* VIP 徽章样式优化:紫色背景 + 左右内边距 */}
<span className='inline-flex items-center justify-center bg-indigo-100 text-indigo-600 text-[11px] font-bold px-2 py-0.5 rounded h-5 align-bottom'>
VIP
</span>
<span className='text-sm text-gray-400 ml-1 leading-none mb-0.5'>
{client.id}
</span>
</div>
{/* 右侧:仅保留关闭按钮 */}
<button
className='text-gray-400 hover:text-gray-600 transition-colors text-sm'
onClick={onClose}
>
</button>
</div>
<button className='text-xs text-gray-500' onClick={onClose}>
</button>
</div>
<div className='px-4 py-2 border-b flex items-center gap-2 text-xs'>
{tabs.map((t) => {
const isActive = tab === t.key;
const isDone = tabDone[t.key];
return (
<button
key={t.key}
onClick={() => onTabChange(t.key)}
className={`px-3 py-1 rounded-2xl border text-xs ${
isActive
? 'bg-gray-900 text-white border-gray-900'
{/* Tabs 区域 */}
<div className='px-8 py-4 flex items-center justify-between'>
<div className='flex items-center gap-3'>
{tabs.map((t) => {
const isActive = tab === t.key;
const isDone = tabDone[t.key];
return (
<button
key={t.key}
onClick={() => onTabChange(t.key)}
// 样式修改rounded-full (完全圆角), border 颜色调整, 增加选中时的阴影
className={`px-5 py-2 rounded-2xl border text-sm transition-all duration-200 ${isActive
? 'bg-blue-600 text-white border-blue-600 shadow-md shadow-blue-200'
: isDone
? 'bg-gray-100 text-gray-400 border-gray-200'
: 'bg-white text-gray-700'
}`}
>
{t.label}
{t.key === 'addon' && (client.addonCount || 0) > 0 && (
<span className='ml-1 text-[10px] opacity-80'>({client.addonCount})</span>
)}
{((t.key === 'sign' && signDone) || (t.key === 'print' && printDone)) && <span className='ml-1'></span>}
</button>
);
})}
? 'bg-gray-50 text-gray-400 border-gray-100'
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'
}`}
>
{t.label}
{/* 这里保留了原有逻辑,但微调了样式 */}
{t.key === 'addon' && (client.addonCount || 0) > 0 && (
<span className={`text-xs ${isActive ? 'text-blue-100' : 'text-gray-400'}`}>
({client.addonCount})
</span>
)}
{((t.key === 'sign' && signDone) || (t.key === 'print' && printDone)) && (
<span className=''></span>
)}
</button>
);
})}
</div>
{/* 按钮部分:将原本在 Tab 右侧的任何操作可以放这里,或者留空 */}
</div>
<div className='px-4 py-4 bg-gray-50/60'>
{tab === 'detail' && <ExamDetailInfo client={client} />}
{tab === 'sign' && <ExamSignPanel />}
{tab === 'addon' && <ExamAddonPanel client={client} />}
{tab === 'print' && <ExamPrintPanel client={client} />}
</div>
</div>
</div>
);
@@ -101,32 +131,195 @@ const ExamSignPanel = () => (
</div>
);
const ExamAddonPanel = ({ client }: { client: ExamClient }) => (
<div className='grid grid-cols-2 gap-6 text-sm'>
<div>
<div className='font-medium mb-2'></div>
<ul className='space-y-1 text-xs text-gray-600'>
{client.checkedItems.concat(client.pendingItems).map((item, idx) => (
<li key={idx} className='flex items-center justify-between'>
<span>{item}</span>
<span className='text-gray-400 text-[11px]'>{client.checkedItems.includes(item) ? '已检查' : '未检查'}</span>
</li>
))}
</ul>
</div>
<div>
<div className='font-medium mb-2'></div>
<div className='space-y-2 text-xs text-gray-600'>
{['肿瘤标志物筛查', '甲状腺彩超', '骨密度检测'].map((label) => (
<label key={label} className='flex items-center gap-2'>
<input type='checkbox' className='rounded' /> {label}
</label>
))}
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 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>
<h3 className='text-lg font-semibold text-gray-900 mb-2'></h3>
<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'>
{allAddons.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-green-600 hover:bg-green-700 text-white px-6 py-3 text-base font-medium'
disabled={selectedCount === 0}
>
¥{totalCurrent.toFixed(0)}
</Button>
</div>
<Button className='mt-3'></Button>
</div>
</div>
);
);
};
const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
const [phone, setPhone] = useState((client['mobile' as keyof ExamClient] as string | undefined) || '137****9988');

View File

@@ -82,6 +82,128 @@ export const EXAM_CLIENTS: ExamClient[] = [
customerType: '团客',
guidePrinted: true,
addonCount: 2,
addonOptions: [
{
id: 'addon_001',
name: '胸部 CT',
paid: true,
tags: [{ title: '热门', type: 1 }, { title: '肺结节筛查', type: 2 }],
originalPrice: '400.00',
currentPrice: '320.00',
},
{
id: 'addon_002',
name: '肿瘤标志物组合',
paid: true,
tags: [{ title: '医生推荐', type: 3 }],
originalPrice: '300.00',
currentPrice: '260.00',
},
{
id: 'addon_003',
name: '颈动脉超声',
paid: false,
tags: [{ title: '脑卒中风险', type: 2 }],
originalPrice: '260.00',
currentPrice: '220.00',
},
{
id: 'addon_004',
name: '幽门螺杆菌检测',
paid: false,
tags: [{ title: '胃病筛查', type: 2 }],
originalPrice: '180.00',
currentPrice: '150.00',
},
{
id: 'addon_005',
name: '骨密度检查',
paid: false,
tags: [{ title: '骨质疏松', type: 2 }],
originalPrice: '260.00',
currentPrice: '210.00',
},
{
id: 'addon_006',
name: '心脏彩超',
paid: false,
tags: [{ title: '心功能评估', type: 2 }],
originalPrice: '380.00',
currentPrice: '320.00',
},
{
id: 'addon_007',
name: '甲状腺功能全套',
paid: false,
tags: [{ title: '内分泌', type: 2 }],
originalPrice: '260.00',
currentPrice: '230.00',
},
{
id: 'addon_008',
name: '颅脑核磁共振',
paid: false,
tags: [{ title: '高价值项目', type: 1 }],
originalPrice: '1200.00',
currentPrice: '980.00',
},
{
id: 'addon_009',
name: '眼底照相 + 眼压',
paid: false,
tags: [{ title: '糖尿病并发症', type: 2 }],
originalPrice: '220.00',
currentPrice: '180.00',
},
{
id: 'addon_010',
name: '女性宫颈癌筛查 (TCT+HPV)',
paid: false,
tags: [{ title: '女性建议加选', type: 3 }],
originalPrice: '600.00',
currentPrice: '520.00',
},
{
id: 'addon_011',
name: '男性前列腺专项',
paid: false,
tags: [{ title: '男性专项', type: 2 }],
originalPrice: '260.00',
currentPrice: '220.00',
},
{
id: 'addon_012',
name: '脑血管 CT (CTA)',
paid: false,
tags: [{ title: '高风险人群', type: 1 }],
originalPrice: '1500.00',
currentPrice: '1200.00',
},
{
id: 'addon_013',
name: '肝纤维化评估',
paid: false,
tags: [{ title: '肝病风险', type: 2 }],
originalPrice: '360.00',
currentPrice: '300.00',
},
{
id: 'addon_014',
name: '全身动脉硬化筛查',
paid: false,
tags: [{ title: '血管评估', type: 2 }],
originalPrice: '480.00',
currentPrice: '420.00',
},
{
id: 'addon_015',
name: '睡眠呼吸监测',
paid: false,
tags: [{ title: '打鼾/睡眠差', type: 2 }],
originalPrice: '520.00',
currentPrice: '460.00',
},
],
},
{
id: 'A002',

View File

@@ -88,7 +88,7 @@ export const MainLayout = () => {
operatorName={operatorName}
onLoginClick={() => setLoginModalOpen(true)}
/>
<main className='p-6 space-y-6 flex-1 overflow-auto'>
<main className='p-6 flex-1 overflow-auto'>
<Outlet context={{ search, setSearch }} />
</main>
</div>