加项样式
This commit is contained in:
481
API.md
Normal file
481
API.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# API文档
|
||||
|
||||
URL 以及 接口结构仅供参考。无需完全对应
|
||||
|
||||
## 接口定义(JSON 请求 / 响应)
|
||||
|
||||
### 1.1 发送验证码 `POST /api/v1/auth/send-code`
|
||||
- `传入 手机号`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"phone": "18888888888"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "验证码已发送",
|
||||
"data": {
|
||||
"expiresIn": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 登录 `POST /api/v1/auth/login`
|
||||
- `传入 手机号 验证码`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"phone": "18888888888",
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "jwt-token",
|
||||
"user": {
|
||||
"id": "user_001",
|
||||
"name": "操作员",
|
||||
"role": "admin",
|
||||
"permissions": [
|
||||
"view_revenue",
|
||||
"view_exam",
|
||||
"manage_meal_status",
|
||||
"edit_notes"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.1 获取今日体检统计 `GET /api/v1/home/daily-stats`
|
||||
- `获取:今日预约人数 已签到人数 在检人数 已打印导检单数量 已完成人数 已用餐人数`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"dailyAppointment": 80,
|
||||
"signedIn": 60,
|
||||
"inExam": 25,
|
||||
"guidePrinted": 40,
|
||||
"completed": 30,
|
||||
"meal": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.1 获取营收统计(管理员)`GET /api/v1/home/revenue-stats`
|
||||
- `获取:体检收入 加项收入 整体收入 目标收入`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"examRevenue": 86000,
|
||||
"addonRevenue": 12400,
|
||||
"totalRevenue": 98400,
|
||||
"targetRevenue": 120000,
|
||||
"gap": 21600
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.1 获取 B1 数据 `GET /api/v1/home/b1-dashboard`
|
||||
- `获取:当前客户总数 待检人数 在检人数`
|
||||
- `列表:科室 医生 已检人数 已检部位数 总时长 平均时长`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"summary": {
|
||||
"totalClients": 33,
|
||||
"waiting": 10,
|
||||
"inExam": 10
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"department": "B超1",
|
||||
"doctor": "张医生",
|
||||
"examinedCount": 6,
|
||||
"examinedParts": 18,
|
||||
"totalDuration": 90,
|
||||
"avgDuration": 15,
|
||||
"inExam": 2,
|
||||
"waiting": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.1 获取北3数据 `GET /api/v1/home/north3-dashboard`
|
||||
- `获取:今日家医生数 分配客户数 面诊数`
|
||||
- `列表:家医 面诊率 分配客户数 面诊数`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"summary": {
|
||||
"totalDoctors": 3,
|
||||
"totalAssigned": 45,
|
||||
"consultations": 26
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"doctor": "刘医生",
|
||||
"assignmentCount": 15,
|
||||
"consultations": 9,
|
||||
"consultRate": 0.6
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.1 获取体检客户列表 `GET /api/v1/exam/clients`
|
||||
- `条件:时间段,如 8:00 ~ 12:00 到 12:00 ~ 17:00(或者上午0下午1)`
|
||||
- `条件:用户等级,如高客 普客 散客 团客`
|
||||
- `条件:登记情况,如已登记 未登记`
|
||||
- `获取:套餐名称 渠道 状态 签到时间(耗时起始时间) 是否已签到 已加项数量 是否打印体检单`
|
||||
- **查询参数**
|
||||
```json
|
||||
{
|
||||
"timeRange": "08:00-12:00",
|
||||
"level": "高客",
|
||||
"customerType": "团客",
|
||||
"signStatus": "已登记",
|
||||
"search": "张",
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"list": [
|
||||
{
|
||||
"id": "A001",
|
||||
"name": "张伟",
|
||||
"packageName": "高端入职体检套餐",
|
||||
"channel": "团体客户",
|
||||
"status": "体检中",
|
||||
"signedIn": true,
|
||||
"signTime": "2025-11-18 08:55",
|
||||
"elapsed": "00:45",
|
||||
"addonCount": 2,
|
||||
"guidePrinted": true,
|
||||
"timeSlot": "上午",
|
||||
"level": "VIP",
|
||||
"vipType": "高客",
|
||||
"customerType": "团客"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.1 获取体检客户详情 `GET /api/v1/exam/clients/{clientId}`
|
||||
- `基础信息:姓名 身份证 手机号 性别 年龄 客户级别 渠道 婚姻 家医 团标签`
|
||||
- `体检进度:已查项目 未查项目 弃检项目 延期项目`
|
||||
- `历史记录`
|
||||
- `加项内容`
|
||||
- `该次体检是否上传身份证(或已上传的身份证信息)`
|
||||
- `该次体检是否已签名(或已上传的签名图片)`
|
||||
- `体检知情同意书`
|
||||
- `可选的加项列表(可以用字段表明已经支付后的加项)`
|
||||
- `导检单PDF`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"id": "A001",
|
||||
"basicInfo": {
|
||||
"name": "张伟",
|
||||
"idCard": "4401********1234",
|
||||
"mobile": "137****9988",
|
||||
"gender": "男",
|
||||
"age": 35,
|
||||
"level": "VIP",
|
||||
"channel": "团检",
|
||||
"maritalStatus": "未婚",
|
||||
"familyDoctor": "李医生",
|
||||
"groupTag": "团检"
|
||||
},
|
||||
"progress": {
|
||||
"checkedItems": ["签到", "更衣"],
|
||||
"pendingItems": ["家医面诊"],
|
||||
"abandonedItems": [],
|
||||
"deferredItems": []
|
||||
},
|
||||
"history": [],
|
||||
"addonSummary": [
|
||||
{
|
||||
"name": "肿瘤标志物筛查",
|
||||
"paid": true,
|
||||
"tags": [
|
||||
{
|
||||
"title": "热门",
|
||||
"type": 1,
|
||||
},
|
||||
{
|
||||
"title": "肺结节筛查",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"title": "医生推荐",
|
||||
"type": 3,
|
||||
},
|
||||
{
|
||||
"title": "渠道八折",
|
||||
"type": 4,
|
||||
}
|
||||
],
|
||||
"originalPrice": "320.00",
|
||||
"currentPrice": "240.00"
|
||||
}
|
||||
],
|
||||
"idCardUploaded": true,
|
||||
"idCardImages": {
|
||||
"front": "https://...",
|
||||
"back": "https://..."
|
||||
},
|
||||
"consentSigned": true,
|
||||
"consentImage": "https://...",
|
||||
"consentDocumentUrl": "https://...",
|
||||
"addonOptions": [
|
||||
{
|
||||
"id": "addon_001",
|
||||
"name": "甲状腺彩超",
|
||||
"price": 300,
|
||||
"paid": false
|
||||
}
|
||||
],
|
||||
"guidePdfUrl": "https://example.com/guides/A001.pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 上传身份证 `POST /api/v1/exam/clients/{clientId}/id-card`
|
||||
- `具体用户上传身份证正面、反面`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"frontImage": "base64-front",
|
||||
"backImage": "base64-back"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "身份证上传成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 上传体检同意书签名 `POST /api/v1/exam/clients/{clientId}/consent`
|
||||
- `上传体检同意书签名图片`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"signatureImage": "base64-signature",
|
||||
"documentVersion": "v1.0"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "签名已保存"
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 生成加项订单 `POST /api/v1/exam/clients/{clientId}/addon-orders`
|
||||
- `选择加项内容后获取订单号以及支付二维码`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"addonIds": ["addon_001", "addon_002"],
|
||||
"paymentMethod": "wechat_qr", // 如果是聚合码则取消
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"orderId": "order_001",
|
||||
"amount": 800,
|
||||
"qrcodeUrl": "https://example.com/pay/order_001.png",
|
||||
"expiresIn": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.7 查询订单支付状态 `GET /api/v1/payments/{orderId}`
|
||||
- `根据订单号查询 是否已经支付成功(HTTP或SSE)`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"orderId": "order_001",
|
||||
"status": "paid",
|
||||
"paidAt": "2025-11-18T09:20:00+08:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **SSE(可选)** `GET /api/v1/payments/{orderId}/events`
|
||||
- 服务端推送字段:`event`=`status`, `data`=`{"status":"paid"}`。
|
||||
|
||||
### 9.0 获取客服咨询信息 `GET /api/v1/support/info`
|
||||
- `获取 标题 内容 图片`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"title": "客服咨询 · 圆圆客服台卡",
|
||||
"content": "一站式健康服务说明",
|
||||
"imageUrl": "https://example.com/assets/support-card.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.1 获取用餐状态列表 `GET /api/v1/meals`
|
||||
- `条件 全部 已用餐 未用餐 姓名 体检号`
|
||||
- **查询参数**
|
||||
```json
|
||||
{
|
||||
"status": "all",
|
||||
"clientName": "张",
|
||||
"clientId": "A001"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": [
|
||||
{
|
||||
"id": "A001",
|
||||
"name": "张伟",
|
||||
"status": "体检中",
|
||||
"mealStatus": "done",
|
||||
"mealTime": "2025-11-18 09:30"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 更新用餐状态 `POST /api/v1/meals/{clientId}`
|
||||
- `更新指定用户用餐状态`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"mealStatus": "done"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "用餐状态已更新"
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 获取太平 VIP 认证二维码 `GET /api/v1/support/taiping-vip`
|
||||
- `获取 标题 内容 图片`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"title": "太平 VIP 认证",
|
||||
"content": "扫描二维码完成认证",
|
||||
"imageUrl": "https://example.com/assets/taiping-vip.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.1 获取报告寄送登记二维码 `GET /api/v1/support/report-delivery`
|
||||
- `获取 客服名称 图片`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"serviceName": "圆圆客服",
|
||||
"imageUrl": "https://example.com/assets/delivery-qrcode.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 报告寄送登记 `POST /api/v1/support/report-delivery`
|
||||
- `传入 收件人 手机号 地址`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"recipient": "李四",
|
||||
"phone": "13800000000",
|
||||
"address": "上海市浦东新区世纪大道100号10楼",
|
||||
"remark": "加急寄送"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "寄送信息已保存",
|
||||
"data": {
|
||||
"deliveryId": "delivery_001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 保存操作员备注 `POST /api/v1/notes`
|
||||
- `保存操作员备注内容`
|
||||
- **请求**
|
||||
```json
|
||||
{
|
||||
"operatorId": "user_001",
|
||||
"content": "客户希望下午完成全部项目"
|
||||
}
|
||||
```
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "备注已保存"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 获取操作员备注 `GET /api/v1/notes`
|
||||
- `获取操作员备注内容`
|
||||
- **响应**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": [
|
||||
{
|
||||
"operatorId": "user_001",
|
||||
"content": "客户希望下午完成全部项目",
|
||||
"createdAt": "2025-11-18 10:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -30,19 +30,39 @@ 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-xs text-gray-500' onClick={onClose}>
|
||||
|
||||
{/* 右侧:仅保留关闭按钮 */}
|
||||
<button
|
||||
className='text-gray-400 hover:text-gray-600 transition-colors text-sm'
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
<div className='px-4 py-2 border-b flex items-center gap-2 text-xs'>
|
||||
</div>
|
||||
|
||||
{/* 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];
|
||||
@@ -50,29 +70,39 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
|
||||
<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'
|
||||
// 样式修改: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'
|
||||
? '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='ml-1 text-[10px] opacity-80'>({client.addonCount})</span>
|
||||
<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>
|
||||
)}
|
||||
{((t.key === 'sign' && signDone) || (t.key === 'print' && printDone)) && <span className='ml-1'>✅</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'>
|
||||
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>
|
||||
<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>
|
||||
<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='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>
|
||||
</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>
|
||||
<Button className='mt-3'>确认加项并生成费用</Button>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExamDetailInfo = ({ client }: { client: ExamClient }) => {
|
||||
const [phone, setPhone] = useState((client['mobile' as keyof ExamClient] as string | undefined) || '137****9988');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user