Kaynağa Gözat

feat(follow-up): 实现复诊管理功能及相关页面

- 新增复诊管理API模块,封装创建、更新、查询和删除接口
- 创建医生复诊管理页面(pages/doctor/manage/followup.vue),支持状态筛选和列表展示
- 调整医生端"邀请复诊"按钮逻辑,改为提示患者主动发起复诊
- 新增患者复诊预约相关页面路径配置
- 完善复诊记录数据模型定义,支持Snowflake ID字符串处理
- 实现分页查询、状态统计等核心功能
- 添加状态转换规则和权限控制逻辑
- 优化异常处理和用户交互体验
mcbaiyun 1 ay önce
ebeveyn
işleme
45512d2aac

+ 175 - 0
docs/复诊管理功能限制与适配问题.md

@@ -0,0 +1,175 @@
+# 复诊管理功能限制与适配问题
+
+本文档旨在分析在采用新的后端接口规范后,原有设计中哪些功能无法实现或需要重新设计,以及相应的解决方案。
+
+## 1. 无法直接实现的原有功能
+
+### 1.1 医生主动邀请复诊功能
+
+**原设计功能**:
+- 医生在"我的病人"页面可以主动邀请患者进行复诊
+- 医生填写预约时间并发送邀请
+- 患者收到邀请后可以接受或拒绝
+
+**无法实现的原因**:
+- 新的接口规范中,只有患者可以创建复诊请求(`POST /follow-up/create`)
+- 医生只能通过更新接口(`PUT /follow-up/{id}`)修改状态或信息
+- 缺少医生主动发起复诊邀请的接口
+
+**影响范围**:
+- `pages/doctor/index/my-patients.vue` 中的"邀请复诊"按钮
+- 相关的弹窗表单和交互逻辑
+
+### 1.2 医生端的复诊管理详情操作
+
+**原设计功能**:
+- 医生可以在复诊管理页面对复诊请求进行"同意"、"拒绝"、"变更时间"等操作
+- 可以向患者发送消息
+
+**受限实现的原因**:
+- 新接口规范中医生只能通过更新接口修改状态,不能直接"同意"或"拒绝"
+- 缺少专门的消息发送接口
+- 状态变更需要符合严格的权限控制规则
+
+**影响范围**:
+- 计划中的 `pages/doctor/manage/followup.vue` 页面
+- 复诊详情页的操作按钮和功能
+
+### 1.3 患者端的预约表单功能
+
+**原设计功能**:
+- 患者在"我的医生"页面点击"预约复诊"后,打开表单选择时间并提交
+- 表单包含医生选择、时间选择器、备注输入等
+
+**受限实现的原因**:
+- 新接口要求创建复诊时必须指定 `doctorUserId`
+- 患者可能绑定了多个医生,需要先选择医生再预约
+- 缺少获取患者绑定医生列表的专用接口(虽然可以通过 [userBinding.ts](file:///d:/慢病APP/uniapp-ts/src/api/userBinding.ts) 实现)
+
+**影响范围**:
+- `pages/patient/index/my-doctor.vue` 中的"预约复诊"功能
+- 相关的预约表单和交互逻辑
+
+## 2. 需要重新设计的功能
+
+### 2.1 复诊状态管理流程
+
+**原设计流程**:
+```
+医生邀请 -> 患者接受/拒绝 -> 医生确认 -> 完成复诊
+```
+
+**新流程适配**:
+```
+患者创建请求 -> 医生确认/取消 -> 完成复诊
+```
+
+**变更说明**:
+- 去除了医生主动邀请环节
+- 患者成为复诊流程的发起者
+- 医生只能对患者发起的请求进行确认或取消操作
+
+### 2.2 权限控制模型
+
+**原设计权限**:
+- 医生可以邀请患者复诊
+- 患者可以接受或拒绝邀请
+- 医生可以管理复诊安排
+
+**新权限模型**:
+- 只有患者可以创建复诊请求
+- 医生只能更新分配给自己的复诊记录
+- 患者只能操作自己的复诊记录
+- 删除操作仅限患者本人
+
+## 3. 接口功能缺失分析
+
+### 3.1 缺少专用医生查询接口
+
+**问题描述**:
+- 没有专门用于查询医生信息的接口
+- 患者在创建复诊请求时无法方便地选择医生
+
+**可能的解决方案**:
+- 复用 [userBinding.ts](file:///d:/慢病APP/uniapp-ts/src/api/userBinding.ts) 中的接口获取绑定的医生列表
+- 在创建复诊请求前先获取患者绑定的医生信息
+
+### 3.2 缺少消息通知机制
+
+**问题描述**:
+- 没有专门的消息发送或通知接口
+- 复诊状态变更时无法及时通知相关用户
+
+**可能的解决方案**:
+- 依赖前端轮询获取最新状态
+- 后续扩展 WebSocket 或其他实时通知机制
+
+### 3.3 缺少预约时间冲突检查
+
+**问题描述**:
+- 创建复诊请求时无法检查医生在指定时间是否已有安排
+- 可能出现时间冲突的情况
+
+**可能的解决方案**:
+- 在前端提供医生已安排复诊的时间列表供参考
+- 创建时通过错误提示告知时间冲突
+
+## 4. 页面功能调整建议
+
+### 4.1 医生端页面调整
+
+**`pages/doctor/index/my-patients.vue`**
+- 移除"邀请复诊"按钮或改为提示信息,告知患者需要主动发起复诊请求
+- 可以添加查看患者复诊历史的入口
+
+**`pages/doctor/manage/followup.vue`(待创建)**
+- 主要展示分配给当前医生的复诊请求
+- 提供状态更新功能(确认、取消、完成)
+- 不再提供"同意/拒绝"操作,改为状态变更
+
+### 4.2 患者端页面调整
+
+**`pages/patient/index/my-doctor.vue`**
+- "预约复诊"功能需要重新设计
+- 需要先选择医生,再选择时间
+- 可以显示当前绑定的医生列表供选择
+
+**患者复诊记录页面**(待创建)
+- 展示患者自己创建的复诊请求
+- 允许修改待确认状态的复诊请求
+- 允许删除自己创建的复诊请求
+
+### 4.3 家属端页面调整
+
+**`pages/patient-family/index/my-family.vue`**
+- "邀请复诊"功能同样受限
+- 可能需要通过切换到患者身份来创建复诊请求
+- 或者提供帮助患者创建复诊请求的辅助功能
+
+## 5. 用户体验优化建议
+
+### 5.1 引导用户正确的操作流程
+
+由于医生无法主动邀请复诊,需要通过以下方式引导用户:
+
+1. 在医生端提示患者需要主动发起复诊请求
+2. 在患者端突出显示复诊预约功能入口
+3. 提供清晰的操作指引和说明
+
+### 5.2 状态变更的实时反馈
+
+由于缺少专门的通知机制,建议:
+
+1. 在关键页面添加自动刷新功能
+2. 状态变更后提供明确的成功提示
+3. 考虑定期轮询更新复诊状态
+
+## 6. 总结
+
+新的后端接口设计更加规范和安全,但与原有的产品设计存在一些不匹配之处。主要问题集中在医生主动邀请复诊功能无法实现,以及部分操作流程需要重新设计。
+
+为了适配新的接口规范,我们需要:
+1. 调整用户操作流程,让患者成为复诊请求的发起者
+2. 重新设计相关页面的交互逻辑
+3. 在现有接口基础上增加辅助功能提升用户体验
+4. 与后端团队沟通是否需要扩展接口以支持更完整的产品功能

+ 181 - 0
docs/复诊管理接口适配方案.md

@@ -0,0 +1,181 @@
+# 复诊管理接口适配方案
+
+本文档旨在分析后端提供的复诊管理接口,并提出前端适配方案,以确保前后端协同工作。
+
+## 后端接口分析
+
+根据后端提供的接口设计,复诊管理功能包含以下接口:
+
+1. **创建复诊请求接口**
+   - URL: POST /follow-up/create
+   - 应用场景: 患者创建复诊预约请求
+   - 详细描述: 患者选择医生并指定预约时间,提交复诊申请,系统默认状态为"PENDING"(待确认)
+
+2. **更新复诊记录接口**
+   - URL: PUT /follow-up/{id}
+   - 应用场景: 更新复诊记录(医生确认、取消或患者修改)
+   - 详细描述: 
+     - 医生可以确认、取消或完成复诊
+     - 患者可以修改自己待确认的复诊请求
+     - 当状态更新为"COMPLETED"时,系统会自动记录实际就诊时间
+
+3. **分页查询复诊记录接口**
+   - URL: POST /follow-up/list
+   - 应用场景: 根据时间范围和分页参数查询复诊记录
+   - 详细描述:
+     - 患者查看自己的复诊记录
+     - 医生查看分配给自己的复诊记录
+     - 系统管理员查看所有复诊记录
+
+4. **医生分页查询患者复诊记录接口**
+   - URL: POST /follow-up/list-by-patient
+   - 应用场景: 医生查询特定患者的复诊记录(需有绑定关系或权限)
+   - 详细描述:
+     - 医生查看患者的复诊历史
+     - 系统管理员查看任何患者的复诊记录
+
+5. **删除复诊记录接口**
+   - URL: DELETE /follow-up/{id}
+   - 应用场景: 根据ID删除复诊记录
+   - 详细描述:
+     - 患者删除自己待确认的复诊请求
+     - 仅允许患者本人删除自己的复诊记录
+
+## 前端适配方案
+
+### 1. API模块设计
+
+需要创建一个新的API模块 `src/api/followUp.ts` 来封装所有复诊相关接口调用。
+
+#### 接口映射关系:
+
+| 原设计接口 | 新接口 | 方法名 |
+|-----------|--------|--------|
+| POST /followup/invite | POST /follow-up/create | createFollowUp |
+| POST /followup/accept | PUT /follow-up/{id} | updateFollowUp |
+| POST /followup/decline | PUT /follow-up/{id} | updateFollowUp |
+| GET /followup/list | POST /follow-up/list | getFollowUpList |
+| GET /appointment/list | POST /follow-up/list | getFollowUpList |
+| POST /appointment/create | POST /follow-up/create | createFollowUp |
+
+### 2. 数据模型定义
+
+需要定义相应的数据模型来匹配接口数据格式:
+
+```typescript
+// 复诊记录实体
+interface FollowUp {
+  id: number;
+  patientUserId: number;
+  doctorUserId: number;
+  scheduledTime: string; // 预约时间
+  actualTime?: string;   // 实际就诊时间
+  status: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'; // 状态
+  remark?: string;       // 备注
+  createdAt: string;     // 创建时间
+  updatedAt: string;     // 更新时间
+}
+
+// 分页查询参数
+interface FollowUpQueryParams {
+  patientUserId?: number;
+  doctorUserId?: number;
+  status?: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED';
+  startTime?: string;
+  endTime?: string;
+  current?: number;
+  size?: number;
+}
+
+// 创建复诊请求参数
+interface CreateFollowUpRequest {
+  doctorUserId: number;
+  scheduledTime: string;
+  remark?: string;
+}
+
+// 更新复诊请求参数
+interface UpdateFollowUpRequest {
+  status: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED';
+  scheduledTime?: string;
+  actualTime?: string;
+  remark?: string;
+}
+```
+
+### 3. 页面功能调整
+
+#### 医生端功能调整
+
+1. **医生首页(todayReminders.followUpCount)**
+   - 继续使用现有接口获取待处理复诊数量
+   - 需要确认是否仍使用 `GET https://wx.baiyun.work/doctor/today_reminders`
+
+2. **我的病人(inviteRevisit)**
+   - 将原来的占位功能改为实际调用新接口
+   - 使用 `createFollowUp` 方法创建复诊请求
+   - 弹窗需要包含时间选择器和备注输入框
+
+3. **复诊管理列表(/pages/doctor/manage/followup)**
+   - 使用 `getFollowUpList` 获取复诊记录列表
+   - 支持按状态筛选(PENDING, CONFIRMED, COMPLETED, CANCELLED)
+   - 支持查看详情和操作(确认、取消、完成)
+
+#### 患者端功能调整
+
+1. **我的医生(makeAppointment)**
+   - 将原来的占位功能改为实际调用新接口
+   - 使用 `createFollowUp` 方法创建复诊请求
+   - 弹窗需要包含医生选择、时间选择器和备注输入框
+
+2. **患者复诊记录列表**
+   - 使用 `getFollowUpList` 获取自己的复诊记录
+   - 支持查看详情和修改待确认的复诊请求
+   - 支持删除自己待确认的复诊请求
+
+#### 家属端功能调整
+
+1. **我的家人(inviteRevisit)**
+   - 与医生端类似,改为实际调用新接口
+   - 使用 `createFollowUp` 方法创建复诊请求
+
+### 4. 状态管理和权限控制
+
+1. **状态转换规则**
+   - PENDING -> CONFIRMED (医生确认)
+   - PENDING -> CANCELLED (医生或患者取消)
+   - CONFIRMED -> COMPLETED (医生标记完成,并记录实际就诊时间)
+   - CONFIRMED -> CANCELLED (医生或患者取消)
+
+2. **权限控制**
+   - 患者只能操作自己的复诊记录
+   - 医生只能操作分配给自己的复诊记录
+   - 只有患者能删除自己待确认的复诊请求
+   - 状态更新需要符合状态转换规则
+
+### 5. 异常处理和用户体验优化
+
+1. **异常值提醒增强**
+   - 在健康数据异常提醒中增加"立即预约"按钮
+   - 点击后跳转到预约页面或弹出预约表单
+
+2. **实时更新机制**
+   - 复诊状态变更后,及时更新相关页面和提醒数量
+   - 考虑使用WebSocket或轮询机制实现实时更新
+
+## 待办事项清单
+
+- [ ] 创建 `src/api/followUp.ts` API模块
+- [ ] 定义相关数据模型和接口类型
+- [ ] 修改医生端"邀请复诊"功能,替换为实际接口调用
+- [ ] 修改患者端"预约复诊"功能,替换为实际接口调用
+- [ ] 修改家属端"邀请复诊"功能,替换为实际接口调用
+- [ ] 创建医生复诊管理列表页面 (`/pages/doctor/manage/followup`)
+- [ ] 创建患者复诊记录列表页面
+- [ ] 实现复诊详情查看和状态更新功能
+- [ ] 增强异常值提醒中的预约功能
+- [ ] 添加适当的加载状态和错误处理
+
+## 总结
+
+新的后端接口设计更加规范化和完整化,提供了完整的复诊生命周期管理。前端需要相应地调整接口调用方式和业务逻辑实现,同时保持良好的用户体验。重点关注状态管理和权限控制,确保数据安全和操作合规性。

+ 190 - 0
docs/复诊管理涉及的页面和按钮.md

@@ -0,0 +1,190 @@
+# 复诊管理涉及的页面和按钮
+
+本文档详细描述了项目中涉及复诊管理功能的所有页面和按钮,结合现有的前端UI设计和新的后端接口规范,分析每个组件的功能和限制。
+
+## 1. 医生端涉及的页面和按钮
+
+### 1.1 医生首页 (pages/doctor/index/index.vue)
+
+#### 功能按钮:
+
+1. **复诊管理功能卡片**
+   - 位置:功能区域第二行第二个卡片
+   - 文本:标题"复诊管理",子标题"安排复诊随访"
+   - 功能:点击后跳转到 `/pages/doctor/manage/followup` 页面
+   - 状态:占位功能,目前点击会显示"功能正在开发中"的提示
+
+2. **今日提醒 - 待处理复诊**
+   - 位置:今日提醒卡片中的第一个提醒项
+   - 文本:数字显示 `todayReminders.followUpCount`,标签"待处理复诊"
+   - 功能:点击后跳转到 `/pages/doctor/manage/followup` 页面
+   - 状态:已实现,通过调用 `https://wx.baiyun.work/doctor/today_reminders` 接口获取数据
+
+#### 页面限制:
+- 复诊管理页面 `/pages/doctor/manage/followup` 尚未创建
+- 功能卡片点击后的跳转目标页面不存在
+
+### 1.2 我的病人 (pages/doctor/index/my-patients.vue)
+
+#### 功能按钮:
+
+1. **邀请复诊按钮**
+   - 位置:每个病人卡片底部操作按钮区域的第四个按钮
+   - 文本:"邀请复诊"
+   - 功能:为当前病人发起复诊邀请
+   - 现有实现:调用 [inviteRevisit](file:///d:/慢病APP/uniapp-ts/src/pages/doctor/index/my-patients.vue#L129-L132) 函数,显示"邀请复诊功能开发中"提示
+   - 适配问题:新接口规范中不支持医生主动邀请复诊,只能由患者创建复诊请求
+
+2. **健康数据按钮**
+   - 位置:每个病人卡片底部操作按钮区域的第一个按钮
+   - 文本:"健康数据"
+   - 功能:查看该病人的健康数据
+   - 现有实现:调用 [viewHealthData](file:///d:/慢病APP/uniapp-ts/src/pages/doctor/index/my-patients.vue#L109-L114) 函数,跳转到 `/pages/public/health/index` 并传递参数
+   - 适配问题:此功能不受新接口规范影响
+
+3. **健康动态按钮**
+   - 位置:每个病人卡片底部操作按钮区域的第二个按钮
+   - 文本:"健康动态"
+   - 功能:查看该病人的健康动态
+   - 现有实现:调用 [viewHealthNews](file:///d:/慢病APP/uniapp-ts/src/pages/doctor/index/my-patients.vue#L116-L120) 函数,显示"健康动态功能开发中"提示
+   - 适配问题:占位功能,尚未实现
+
+4. **发送提醒按钮**
+   - 位置:每个病人卡片底部操作按钮区域的第三个按钮
+   - 文本:"发送提醒"
+   - 功能:向该病人发送健康提醒
+   - 现有实现:调用 [sendReminder](file:///d:/慢病APP/uniapp-ts/src/pages/doctor/index/my-patients.vue#L122-L126) 函数,显示"发送提醒功能开发中"提示
+   - 适配问题:占位功能,尚未实现
+
+#### 页面限制:
+- "邀请复诊"功能与新接口规范冲突,无法实现
+- "健康动态"和"发送提醒"功能尚未开发
+
+## 2. 患者端涉及的页面和按钮
+
+### 2.1 我的医生 (pages/patient/index/my-doctor.vue)
+
+#### 功能按钮:
+
+1. **预约复诊按钮**
+   - 位置:医生卡片底部操作按钮区域的第一个按钮
+   - 文本:"预约复诊"
+   - 功能:向当前医生发起复诊预约
+   - 现有实现:调用 [makeAppointment](file:///d:/慢病APP/uniapp-ts/src/pages/patient/index/my-doctor.vue#L141-L146) 函数,显示"预约功能开发中"提示
+   - 适配问题:需要改为调用新的复诊接口创建复诊请求
+
+2. **联系医生按钮**
+   - 位置:医生卡片底部操作按钮区域的第二个按钮
+   - 文本:"联系医生"
+   - 功能:与当前医生进行沟通
+   - 现有实现:调用 [contactDoctor](file:///d:/慢病APP/uniapp-ts/src/pages/patient/index/my-doctor.vue#L148-L153) 函数,显示"联系医生功能开发中"提示
+   - 适配问题:占位功能,尚未实现
+
+3. **绑定医生按钮**
+   - 位置:在没有绑定医生时显示的空状态区域
+   - 文本:"绑定医生"
+   - 功能:绑定新的医生
+   - 现有实现:调用 [bindDoctor](file:///d:/慢病APP/uniapp-ts/src/pages/patient/index/my-doctor.vue#L159-L164) 函数,显示"绑定医生功能开发中"提示
+   - 适配问题:占位功能,尚未实现
+
+#### 页面限制:
+- "预约复诊"功能需要重新实现以适配新接口
+- "联系医生"和"绑定医生"功能尚未开发
+
+## 3. 家属端涉及的页面和按钮
+
+### 3.1 我的家人 (pages/patient-family/index/my-family.vue)
+
+#### 功能按钮:
+
+1. **邀请复诊按钮**
+   - 位置:每个家人卡片底部操作按钮区域的第四个按钮
+   - 文本:"邀请复诊"
+   - 功能:为当前家人发起复诊邀请
+   - 现有实现:调用 [inviteRevisit](file:///d:/慢病APP/uniapp-ts/src/pages/patient-family/index/my-family.vue#L129-L132) 函数,显示"邀请复诊功能开发中"提示
+   - 适配问题:新接口规范中不支持家属主动邀请复诊,只能由患者创建复诊请求
+
+2. **健康数据按钮**
+   - 位置:每个家人卡片底部操作按钮区域的第一个按钮
+   - 文本:"健康数据"
+   - 功能:查看该家人的健康数据
+   - 现有实现:调用 [viewHealthData](file:///d:/慢病APP/uniapp-ts/src/pages/patient-family/index/my-family.vue#L109-L114) 函数,跳转到 `/pages/public/health/index` 并传递参数
+   - 适配问题:此功能不受新接口规范影响
+
+3. **健康动态按钮**
+   - 位置:每个家人卡片底部操作按钮区域的第二个按钮
+   - 文本:"健康动态"
+   - 功能:查看该家人的健康动态
+   - 现有实现:调用 [viewHealthNews](file:///d:/慢病APP/uniapp-ts/src/pages/patient-family/index/my-family.vue#L116-L120) 函数,显示"健康动态功能开发中"提示
+   - 适配问题:占位功能,尚未实现
+
+4. **发送提醒按钮**
+   - 位置:每个家人卡片底部操作按钮区域的第三个按钮
+   - 文本:"发送提醒"
+   - 功能:向该家人发送健康提醒
+   - 现有实现:调用 [sendReminder](file:///d:/慢病APP/uniapp-ts/src/pages/patient-family/index/my-family.vue#L122-L126) 函数,显示"发送提醒功能开发中"提示
+   - 适配问题:占位功能,尚未实现
+
+#### 页面限制:
+- "邀请复诊"功能与新接口规范冲突,无法实现
+- "健康动态"和"发送提醒"功能尚未开发
+
+## 4. 需要创建的新页面
+
+### 4.1 医生复诊管理页面 (pages/doctor/manage/followup.vue)
+
+#### 功能描述:
+- 展示分配给当前医生的复诊请求列表
+- 提供对复诊请求的状态更新功能(确认、取消、完成)
+- 查看复诊详情
+
+#### 当前状态:
+- 页面尚未创建
+- 需要通过医生首页的"复诊管理"功能卡片或"待处理复诊"提醒项访问
+
+### 4.2 患者复诊记录页面 (待创建)
+
+#### 功能描述:
+- 展示当前患者创建的复诊请求列表
+- 提供创建新的复诊请求功能
+- 提供修改或删除待确认状态的复诊请求功能
+
+#### 当前状态:
+- 页面尚未创建
+- 需要通过患者端相关功能访问
+
+## 5. 功能适配建议
+
+### 5.1 医生端适配建议
+
+1. **移除"邀请复诊"按钮或修改其功能**
+   - 由于新接口不支持医生主动邀请复诊,应移除该按钮
+   - 或者修改为提示信息,告知医生患者需要主动发起复诊请求
+
+2. **创建复诊管理页面**
+   - 实现 `/pages/doctor/manage/followup` 页面
+   - 展示分配给当前医生的复诊请求列表
+   - 提供状态更新功能
+
+### 5.2 患者端适配建议
+
+1. **重新实现"预约复诊"功能**
+   - 修改 [makeAppointment](file:///d:/慢病APP/uniapp-ts/src/pages/patient/index/my-doctor.vue#L141-L146) 函数,调用新的复诊接口
+   - 实现预约表单,包含医生选择、时间选择、原因输入等功能
+
+### 5.3 家属端适配建议
+
+1. **移除"邀请复诊"按钮或修改其功能**
+   - 由于新接口不支持家属主动邀请复诊,应移除该按钮
+   - 或者修改为提示信息,告知家属需要患者主动发起复诊请求
+
+## 6. 总结
+
+目前项目中涉及复诊管理的页面和按钮较多,但大部分功能尚未实现或与新接口规范存在冲突。主要问题包括:
+
+1. 医生和家属端的"邀请复诊"功能无法实现
+2. 多个占位功能需要开发
+3. 缺少关键的复诊管理页面
+4. 需要根据新接口规范重新设计用户操作流程
+
+建议优先开发患者端的复诊预约功能和医生端的复诊管理页面,以建立完整的复诊流程。

+ 121 - 0
docs/复诊管理设计.md

@@ -0,0 +1,121 @@
+# 复诊管理(Follow-up)前端设计提取
+
+本文档从当前项目代码中提取并集中说明与“复诊管理”相关的前端页面、导航、交互与现状(哪些页面已实现、哪些页面为占位/待实现),并给出必要的接口建议与 UI 行为说明,方便前后端对齐与产品设计对接。
+
+---
+
+## 概览
+
+当前仓库涉及与“复诊/预约/随访”相关的前端位置:
+- 医生侧
+  - `pages/doctor/index/index.vue`(医生首页)
+    - 功能入口:功能卡片 -> “复诊管理”(点击跳转 `/pages/doctor/manage/followup`)
+    - 今日提醒卡片展示 `todayReminders.followUpCount`(待处理复诊数量)
+    - 活动区展示 `patientActivities`,其中某些项描述会提到“完成了今日复诊”作为活动示例
+  - `pages/doctor/index/my-patients.vue`(我的病人)
+    - 为每个病人提供 “邀请复诊” 按钮(当前为占位,弹窗提示“邀请复诊功能开发中”)
+  - `pages/doctor/manage/index.vue`(后台管理)
+    - 菜单项包含健康资讯 / 快捷问题 / 药品管理,暂无具体复诊管理页面(仅占位)
+
+- 患者侧
+  - `pages/patient/index/my-doctor.vue`(我的医生)
+    - 提供“预约复诊”按钮(当前占位,弹窗提示“预约功能开发中”)
+  - `pages/patient/index/index.vue`(患者首页)
+    - 页面卡片中有“医生预约 / 一键预约复诊”入口(多为占位)
+
+- 家属
+  - `pages/patient-family/index/my-family.vue`(我的家人)
+    - 提供“邀请复诊”按钮(占位)
+
+- 健康数据提醒触发
+  - `pages/patient/health/details/*`(血压 / 血糖 / 心率 / 体格)
+    - 当用户把某些指标值录入超阈值时,会通过 `uni.showModal` 弹窗建议“建议尽快复诊” —— 这是在数据输入端触发复诊建议的逻辑点。
+
+---
+
+## 现有 UI 元素与交互(逐页面细节)
+
+### 1. `pages/doctor/index/index.vue`(医生首页)
+- 位置/导航
+  - 功能卡片(右侧): 点击 `复诊管理` 调用 `onItemClick('复诊管理')` 并导航至 `'/pages/doctor/manage/followup'`。
+  - 卡片内文字:"复诊管理" / 子标题:"安排复诊随访"
+- 今日提醒(左上)
+  - 显示 `todayReminders.followUpCount`(数字)与标签 "待处理复诊"
+  - 点击该提醒也会跳转 `/pages/doctor/manage/followup`
+- 活动卡
+  - patientActivities list,示例中包含描述如 "患者李四完成了今日复诊"。这是显示最近复诊/活动的 UI 区块。
+
+### 2. `pages/doctor/index/my-patients.vue`(我的病人)
+- 每个病人条目(`.patient-card`)中的操作按钮:
+  - "健康数据" -> 跳转 `/pages/public/health/index?patientId=...&bindingType=DOCTOR`
+  - "健康动态" -> 占位
+  - "发送提醒" -> 占位
+  - "邀请复诊" -> 占位(弹窗提示)
+- Invite 按钮逻辑 
+  - 目前 `inviteRevisit(patient)` 仅调用 `uni.showToast({ title: '邀请复诊功能开发中' })`,无后端调用或模态输入。
+
+### 3. `pages/patient/index/my-doctor.vue`(我的医生)
+- 操作按钮:
+  - "预约复诊" -> `makeAppointment()` -> 占位(弹窗)
+  - "联系医生" -> 占位
+  - "绑定医生" -> 占位(但绑定可由 `userBinding.create` 实现)
+- 用户故事期望:点击预约 -> 打开预约表单/选择时间 -> 提交 -> 后端创建预约 -> 跳转到预约详情或显示成功消息
+
+### 4. `pages/patient-family/index/my-family.vue`(我的家人)
+- 同 `my-patients.vue`:提供 "邀请复诊" 按钮 -> 目前为占位
+
+### 5. `pages/patient/health/details/*`(指标页面)
+- 在 `confirmAdd()` 或提交数据时:
+  - 对异常阈值(如 血压 > 140/90、空腹血糖 > 7.0)弹窗 `uni.showModal({title: '异常', content: '建议尽快复诊'})` 提醒病人。
+  - 该弹窗仅提示,不包含直接预约/联系医生按钮。
+
+---
+
+## 推荐复诊流程(前端设计建议)
+为了把现有占位功能逐步实现,建议采用以下最小可用流程:
+
+1. 医生端:`复诊管理` 列表
+   - 页面 URL:`/pages/doctor/manage/followup`(当前缺失,需创建)
+   - 列表展示:待处理/已完成的复诊邀请(字段:id、patientName、patientAvatar、status, scheduledTime, createdAt)
+   - 操作:查看 -> 详情页(可触发: 同意、拒绝、变更时间、发送消息)
+   - 新增:医生可以手动发起 `邀请复诊`(直接从 `我的病人` 发起),或基于预设规则(异常数据触发复诊建议)创建新的复诊任务
+2. 医生向患者发起复诊邀请
+   - 弹窗表单:选择患者、选择/输入时间、可选备注
+   - 后端:POST /followup/invite
+   - 前端:调用后刷新 `followup` 列表并更新 `todayReminders.followUpCount`
+3. 患者端:预约/接收复诊邀请
+   - 患者接收邀请后:可接受/拒绝或重新选择时间
+   - 管理患者预约列表:`/pages/patient/appointments`(未来新增)
+4. 健康数据自动建议复诊
+   - 当 `confirmAdd()` 检测到异常时,除了 `showModal`,增加 "现在预约" 按钮或 "联系医生",直接跳转到 `my-doctor` -> 预约交互,或弹出预约表单
+
+---
+
+## 已知数据/接口(前端已使用或建议)
+- 已在页面调用但未封装到 `src/api`:
+  - GET `https://wx.baiyun.work/doctor/today_reminders` —— 返回 `{ followUpCount, abnormalCount }`
+  - GET `https://wx.baiyun.work/doctor/patient_activities` —— 返回列表活动
+- 建议新增(后端与前端协作)接口:
+  - GET /followup/list?doctorUserId=...&status=... -- 复诊列表
+  - POST /followup/invite -- 邀请复诊(doctor -> patient)
+    - body: { patientUserId, scheduledTime, remark }
+  - POST /followup/accept -- 病人接受邀请
+  - POST /followup/decline -- 病人拒绝邀请
+  - GET /appointment/list?userId=... -- 患者预约列表
+  - POST /appointment/create -- 患者创建预约
+
+---
+
+## UI/UX 注意点与改进建议
+- `inviteRevisit` / `makeAppointment` 的占位按钮应支持模态表单输入(时间、备注),并在提交成功后把患者活动和医生 `todayReminders.followUpCount` 更新。
+- 与 `user-binding` 集成:邀请/预约时须确保 `patientUserId` 的可用性与安全权限(仅当医生与患者绑定时允许邀请)。
+- 在异常值提示中(`showModal`),建议加入可操作按钮(如“现在预约”)以提升转化。
+
+---
+
+## 当前缺失/待实现要点(行动项)
+- [ ] 创建 `pages/doctor/manage/followup.vue` 页面骨架,并实现最小列表展示及跳转功能。
+- [ ] 在 `src/api` 中新增 `followup.ts`,封装 `list/invite/accept/decline` 接口并使用统一请求包装 `src/api/request.ts`。
+- [ ] 在 `my-patients` 和 `my-family` 中把 `inviteRevisit` 从占位修改为调用 `followup/invite`,并添加预约对话框(time picker)
+- [ ] 在患者`my-doctor` 页面实现预约交互:打开选择时间 -> POST `/appointment/create`
+- [ ] 在异常检测逻辑(数据页)添加“立即预约”动作:跳转到预约流程或直接发起 

+ 199 - 0
src/api/followUp.ts

@@ -0,0 +1,199 @@
+import request from './request'
+
+// 复诊记录实体
+export interface FollowUp {
+  id: string // Snowflake 64位ID 使用字符串保存以避免 JS Number 精度丢失
+  patientUserId: string
+  doctorUserId: string
+  appointmentTime: string // 预约时间
+  actualTime?: string // 实际就诊时间
+  status: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' // 状态
+  reason?: string // 复诊原因
+  notes?: string // 备注信息
+  createTime: string // 创建时间
+  updateTime: string // 更新时间
+  patientNickname?: string // 患者昵称
+  doctorNickname?: string // 医生昵称
+}
+
+// 分页查询参数
+export interface FollowUpQueryParams {
+  pageNum?: number
+  pageSize?: number
+  startTime?: string
+  endTime?: string
+}
+
+// 医生查询患者复诊记录参数
+export interface DoctorListByPatientParams extends FollowUpQueryParams {
+  patientUserId: string | number
+}
+
+// 创建复诊请求参数
+export interface CreateFollowUpRequest {
+  doctorUserId: string | number
+  appointmentTime: string
+  reason?: string
+}
+
+// 更新复诊请求参数
+export interface UpdateFollowUpRequest {
+  appointmentTime?: string
+  status?: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'
+  notes?: string
+  reason?: string
+}
+
+// 分页响应
+export interface FollowUpPageResponse {
+  records: FollowUp[]
+  total: number
+  size: number
+  current: number
+  orders: Array<{ column: string; asc: boolean }>
+  optimizeCountSql: boolean
+  searchCount: boolean
+  optimizeJoinOfCountSql: boolean
+  maxLimit: number
+  countId: string
+  pages: number
+}
+
+/**
+ * 创建复诊请求
+ * @param payload 创建复诊请求参数
+ */
+export async function createFollowUp(payload: CreateFollowUpRequest) {
+  const res: any = await request({
+    url: 'https://wx.baiyun.work/follow-up/create',
+    method: 'POST',
+    header: { 'Content-Type': 'application/json' },
+    data: payload
+  })
+  
+  // 强制把 Snowflake ID 字段转换为字符串,避免后续使用 Number 导致精度丢失
+  try {
+    const parsed = res?.data as any
+    if (parsed && parsed.code === 200 && parsed.data) {
+      parsed.data = {
+        ...parsed.data,
+        id: String(parsed.data.id),
+        patientUserId: String(parsed.data.patientUserId),
+        doctorUserId: String(parsed.data.doctorUserId)
+      }
+      res.data = parsed
+    }
+  } catch (e) {
+    // ignore
+  }
+  
+  return res
+}
+
+/**
+ * 更新复诊记录
+ * @param id 复诊记录ID
+ * @param payload 更新复诊请求参数
+ */
+export async function updateFollowUp(id: string | number, payload: UpdateFollowUpRequest) {
+  const res: any = await request({
+    url: `https://wx.baiyun.work/follow-up/${encodeURIComponent(String(id))}`,
+    method: 'PUT',
+    header: { 'Content-Type': 'application/json' },
+    data: payload
+  })
+  
+  // 强制把 Snowflake ID 字段转换为字符串,避免后续使用 Number 导致精度丢失
+  try {
+    const parsed = res?.data as any
+    if (parsed && parsed.code === 200 && parsed.data) {
+      parsed.data = {
+        ...parsed.data,
+        id: String(parsed.data.id),
+        patientUserId: String(parsed.data.patientUserId),
+        doctorUserId: String(parsed.data.doctorUserId)
+      }
+      res.data = parsed
+    }
+  } catch (e) {
+    // ignore
+  }
+  
+  return res
+}
+
+/**
+ * 分页查询复诊记录
+ * @param query 查询参数
+ */
+export async function getFollowUpList(query: FollowUpQueryParams) {
+  const res: any = await request({
+    url: 'https://wx.baiyun.work/follow-up/list',
+    method: 'POST',
+    header: { 'Content-Type': 'application/json' },
+    data: query
+  })
+  
+  // 强制把 Snowflake ID 字段转换为字符串,避免后续使用 Number 导致精度丢失
+  try {
+    const parsed = res?.data as any
+    if (parsed && parsed.code === 200 && parsed.data && Array.isArray(parsed.data.records)) {
+      parsed.data.records = parsed.data.records.map((r: any) => ({
+        ...r,
+        id: String(r.id),
+        patientUserId: String(r.patientUserId),
+        doctorUserId: String(r.doctorUserId)
+      }))
+      res.data = parsed
+    }
+  } catch (e) {
+    // ignore
+  }
+  
+  return res
+}
+
+/**
+ * 医生分页查询患者复诊记录
+ * @param params 查询参数,包含patientUserId
+ * @param query 查询条件
+ */
+export async function getFollowUpListByPatient(params: DoctorListByPatientParams, query: Omit<FollowUpQueryParams, 'patientUserId'>) {
+  const qs = `?patientUserId=${encodeURIComponent(String(params.patientUserId))}`
+  const res: any = await request({
+    url: 'https://wx.baiyun.work/follow-up/list-by-patient' + qs,
+    method: 'POST',
+    header: { 'Content-Type': 'application/json' },
+    data: query
+  })
+  
+  // 强制把 Snowflake ID 字段转换为字符串,避免后续使用 Number 导致精度丢失
+  try {
+    const parsed = res?.data as any
+    if (parsed && parsed.code === 200 && parsed.data && Array.isArray(parsed.data.records)) {
+      parsed.data.records = parsed.data.records.map((r: any) => ({
+        ...r,
+        id: String(r.id),
+        patientUserId: String(r.patientUserId),
+        doctorUserId: String(r.doctorUserId)
+      }))
+      res.data = parsed
+    }
+  } catch (e) {
+    // ignore
+  }
+  
+  return res
+}
+
+/**
+ * 删除复诊记录
+ * @param id 复诊记录ID
+ */
+export async function deleteFollowUp(id: string | number) {
+  const res: any = await request({
+    url: `https://wx.baiyun.work/follow-up/${encodeURIComponent(String(id))}`,
+    method: 'DELETE'
+  })
+  return res
+}

+ 18 - 0
src/pages.json

@@ -102,6 +102,12 @@
 				"navigationBarTitleText": "患者建档信息"
 			}
 		},
+		{
+			"path": "pages/patient/profile/infos/followup-edit",
+			"style": {
+				"navigationBarTitleText": "编辑复诊预约"
+			}
+		},
 		{
 			"path": "pages/patient/index/my-doctor",
 			"style": {
@@ -173,6 +179,18 @@
 			"style": {
 				"navigationBarTitleText": "我的病人"
 			}
+		},
+		{
+			"path": "pages/doctor/manage/followup",
+			"style": {
+				"navigationBarTitleText": "复诊管理"
+			}
+		},
+		{
+			"path": "pages/patient/profile/infos/followup-request",
+			"style": {
+				"navigationBarTitleText": "申请复诊"
+			}
 		}
 	],
 	"globalStyle": {

+ 7 - 5
src/pages/doctor/index/my-patients.vue

@@ -19,7 +19,7 @@
         <button class="action-btn primary" @click="viewHealthData(patient)">健康数据</button>
         <button class="action-btn secondary" @click="viewHealthNews(patient)">健康动态</button>
         <button class="action-btn secondary" @click="sendReminder(patient)">发送提醒</button>
-        <button class="action-btn secondary" @click="inviteRevisit(patient)">邀请复诊</button>
+        <button class="action-btn secondary" @click="inviteRevisitInfo">邀请复诊</button>
       </view>
     </view>
     
@@ -156,10 +156,12 @@ const sendReminder = (patient: PatientInfo) => {
   })
 }
 
-const inviteRevisit = (patient: PatientInfo) => {
-  uni.showToast({
-    title: '邀请复诊功能开发中',
-    icon: 'none'
+const inviteRevisitInfo = () => {
+  uni.showModal({
+    title: '提示',
+    content: '根据最新规定,医生不能主动邀请复诊。患者需要在患者端主动发起复诊请求。',
+    showCancel: false,
+    confirmText: '知道了'
   })
 }
 

+ 432 - 0
src/pages/doctor/manage/followup.vue

@@ -0,0 +1,432 @@
+<template>
+  <CustomNav title="复诊管理" leftType="back" />
+  <view class="page-container">
+    <view class="tabs">
+      <view 
+        class="tab" 
+        :class="{ active: activeTab === 'pending' }" 
+        @click="activeTab = 'pending'"
+      >
+        待处理({{ pendingCount }})
+      </view>
+      <view 
+        class="tab" 
+        :class="{ active: activeTab === 'confirmed' }" 
+        @click="activeTab = 'confirmed'"
+      >
+        已确认({{ confirmedCount }})
+      </view>
+      <view 
+        class="tab" 
+        :class="{ active: activeTab === 'completed' }" 
+        @click="activeTab = 'completed'"
+      >
+        已完成({{ completedCount }})
+      </view>
+      <view 
+        class="tab" 
+        :class="{ active: activeTab === 'cancelled' }" 
+        @click="activeTab = 'cancelled'"
+      >
+        已取消({{ cancelledCount }})
+      </view>
+    </view>
+
+    <view class="followup-list" v-if="filteredFollowups.length > 0">
+      <view 
+        class="followup-card" 
+        v-for="followup in filteredFollowups" 
+        :key="followup.id"
+      >
+        <view class="card-header">
+          <view class="patient-info">
+            <text class="patient-name">{{ followup.patientNickname || '未知患者' }}</text>
+            <text class="request-time">{{ formattedFollowups[followup.id]?.requestTime }}</text>
+          </view>
+          <view class="status-badge" :class="followup.status">
+            {{ getStatusText(followup.status) }}
+          </view>
+        </view>
+        
+        <view class="card-body">
+          <view class="info-row">
+            <text class="label">预约时间:</text>
+            <text class="value">{{ formattedFollowups[followup.id]?.scheduledTime }}</text>
+          </view>
+          <view class="info-row">
+            <text class="label">复诊原因:</text>
+            <text class="value">{{ followup.reason || '无' }}</text>
+          </view>
+        </view>
+        
+        <view class="card-footer" v-if="followup.status === 'PENDING'">
+          <button class="action-btn cancel" @click="cancelFollowup(followup.id)">取消</button>
+          <button class="action-btn confirm" @click="confirmFollowup(followup.id)">确认</button>
+        </view>
+        
+        <view class="card-footer" v-else-if="followup.status === 'CONFIRMED'">
+          <button class="action-btn cancel" @click="cancelFollowup(followup.id)">取消</button>
+          <button class="action-btn complete" @click="completeFollowup(followup.id)">标记为完成</button>
+        </view>
+      </view>
+    </view>
+    
+    <view class="empty-state" v-else>
+      <image class="empty-icon" src="/static/icons/remixicon/article-line.svg" />
+      <text class="empty-text">暂无复诊请求</text>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import CustomNav from '@/components/custom-nav.vue'
+import { formatDate } from '@/utils/date'
+import { getFollowUpList, updateFollowUp } from '@/api/followUp'
+import type { FollowUp } from '@/api/followUp'
+
+type FollowupRequest = FollowUp
+
+const activeTab = ref<'pending' | 'confirmed' | 'completed' | 'cancelled'>('pending')
+const followups = ref<FollowupRequest[]>([])
+
+// 统计各状态数量
+const pendingCount = computed(() => followups.value.filter(f => f.status === 'PENDING').length)
+const confirmedCount = computed(() => followups.value.filter(f => f.status === 'CONFIRMED').length)
+const completedCount = computed(() => followups.value.filter(f => f.status === 'COMPLETED').length)
+const cancelledCount = computed(() => followups.value.filter(f => f.status === 'CANCELLED').length)
+
+// 根据标签页过滤的复诊数据
+const filteredFollowups = computed(() => {
+  switch (activeTab.value) {
+    case 'pending':
+      return followups.value.filter(f => f.status === 'PENDING')
+    case 'confirmed':
+      return followups.value.filter(f => f.status === 'CONFIRMED')
+    case 'completed':
+      return followups.value.filter(f => f.status === 'COMPLETED')
+    case 'cancelled':
+      return followups.value.filter(f => f.status === 'CANCELLED')
+    default:
+      return followups.value
+  }
+})
+
+// 格式化后的复诊数据
+const formattedFollowups = computed(() => {
+  const formatted: Record<string, { requestTime: string; scheduledTime: string }> = {}
+  followups.value.forEach(followup => {
+    formatted[followup.id] = {
+      requestTime: formatDate(followup.createTime),
+      scheduledTime: formatDate(followup.appointmentTime)
+    }
+  })
+  return formatted
+})
+
+// 获取状态文本
+const getStatusText = (status: string) => {
+  switch (status) {
+    case 'PENDING': return '待处理'
+    case 'CONFIRMED': return '已确认'
+    case 'COMPLETED': return '已完成'
+    case 'CANCELLED': return '已取消'
+    default: return status
+  }
+}
+
+// 获取复诊请求数据
+const fetchFollowups = async () => {
+  uni.showLoading({ title: '加载中...' })
+  
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '未登录',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 获取当前医生ID
+    const userInfo = uni.getStorageSync('user_info')
+    const doctorUserId = userInfo?.id
+    
+    if (!doctorUserId) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '获取医生信息失败',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 调用API获取复诊列表
+    const response: any = await getFollowUpList({})
+    
+    uni.hideLoading()
+    
+    if (response && response.data && response.data.code === 200) {
+      // 修复:使用records的实际长度而不是total字段
+      followups.value = response.data.data.records || []
+    } else {
+      uni.showToast({
+        title: '获取复诊数据失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('获取复诊数据失败:', error)
+    uni.showToast({
+      title: '获取复诊数据失败',
+      icon: 'none'
+    })
+  }
+}
+
+// 取消复诊请求
+const cancelFollowup = (id: string) => {
+  uni.showModal({
+    title: '确认取消',
+    content: '确定要取消这个复诊请求吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 调用API更新复诊请求状态
+        updateFollowUpStatus(id, 'CANCELLED')
+      }
+    }
+  })
+}
+
+// 确认复诊请求
+const confirmFollowup = (id: string) => {
+  uni.showModal({
+    title: '确认复诊',
+    content: '确定要确认这个复诊请求吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 调用API更新复诊请求状态
+        updateFollowUpStatus(id, 'CONFIRMED')
+      }
+    }
+  })
+}
+
+// 完成复诊
+const completeFollowup = (id: string) => {
+  uni.showModal({
+    title: '完成复诊',
+    content: '确定已完成本次复诊吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 调用API更新复诊请求状态
+        updateFollowUpStatus(id, 'COMPLETED')
+      }
+    }
+  })
+}
+
+// 更新复诊请求状态
+const updateFollowUpStatus = async (id: string, status: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED') => {
+  uni.showLoading({ title: '处理中...' })
+  
+  try {
+    const response: any = await updateFollowUp(id, { status })
+    
+    uni.hideLoading()
+    
+    if (response && response.data && response.data.code === 200) {
+      // 更新本地数据
+      const index = followups.value.findIndex(f => f.id === id)
+      if (index !== -1) {
+        followups.value[index].status = status
+      }
+      uni.showToast({ title: '操作成功', icon: 'success' })
+    } else {
+      uni.showToast({
+        title: '操作失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('更新复诊状态失败:', error)
+    uni.showToast({
+      title: '操作失败',
+      icon: 'none'
+    })
+  }
+}
+
+onMounted(() => {
+  fetchFollowups()
+})
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 40rpx;
+}
+
+.tabs {
+  display: flex;
+  background-color: #fff;
+  border-bottom: 1rpx solid #eee;
+}
+
+.tab {
+  flex: 1;
+  text-align: center;
+  padding: 30rpx 0;
+  font-size: 28rpx;
+  color: #666;
+}
+
+.tab.active {
+  color: #3742fa;
+  border-bottom: 4rpx solid #3742fa;
+}
+
+.followup-list {
+  padding: 20rpx;
+}
+
+.followup-card {
+  background-color: #fff;
+  border-radius: 20rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.patient-info {
+  display: flex;
+  flex-direction: column;
+}
+
+.patient-name {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.request-time {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.status-badge {
+  padding: 8rpx 16rpx;
+  border-radius: 10rpx;
+  font-size: 24rpx;
+  color: #fff;
+}
+
+.status-badge.PENDING {
+  background-color: #ffa502;
+}
+
+.status-badge.CONFIRMED {
+  background-color: #3742fa;
+}
+
+.status-badge.COMPLETED {
+  background-color: #2ed573;
+}
+
+.status-badge.CANCELLED {
+  background-color: #ccc;
+}
+
+.card-body {
+  padding: 30rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.info-row {
+  display: flex;
+  margin-bottom: 20rpx;
+}
+
+.info-row:last-child {
+  margin-bottom: 0;
+}
+
+.label {
+  font-size: 28rpx;
+  color: #666;
+  width: 180rpx;
+}
+
+.value {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+}
+
+.card-footer {
+  display: flex;
+  padding: 30rpx;
+  justify-content: flex-end;
+}
+
+.action-btn {
+  padding: 0 30rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  border-radius: 10rpx;
+  font-size: 28rpx;
+  margin-left: 20rpx;
+}
+
+.cancel {
+  background-color: #fff;
+  color: #ff4757;
+  border: 1rpx solid #ff4757;
+}
+
+.confirm {
+  background-color: #3742fa;
+  color: #fff;
+}
+
+.complete {
+  background-color: #2ed573;
+  color: #fff;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 100rpx 40rpx;
+}
+
+.empty-icon {
+  width: 120rpx;
+  height: 120rpx;
+  margin-bottom: 30rpx;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 32rpx;
+  color: #666;
+}
+</style>

+ 7 - 5
src/pages/patient-family/index/my-family.vue

@@ -19,7 +19,7 @@
         <button class="action-btn primary" @click="viewHealthData(family)">健康数据</button>
         <button class="action-btn secondary" @click="viewHealthNews(family)">健康动态</button>
         <button class="action-btn secondary" @click="sendReminder(family)">发送提醒</button>
-        <button class="action-btn secondary" @click="inviteRevisit(family)">邀请复诊</button>
+        <button class="action-btn secondary" @click="inviteRevisitInfo">邀请复诊</button>
       </view>
     </view>
     
@@ -156,10 +156,12 @@ const sendReminder = (family: FamilyInfo) => {
   })
 }
 
-const inviteRevisit = (family: FamilyInfo) => {
-  uni.showToast({
-    title: '邀请复诊功能开发中',
-    icon: 'none'
+const inviteRevisitInfo = () => {
+  uni.showModal({
+    title: '提示',
+    content: '根据最新规定,家属不能主动邀请复诊。患者需要在患者端主动发起复诊请求。',
+    showCancel: false,
+    confirmText: '知道了'
   })
 }
 

+ 342 - 29
src/pages/patient/index/my-doctor.vue

@@ -15,13 +15,46 @@
           <text class="doctor-phone" v-else>联系电话: 未提供</text>
         </view>
       </view>
-      
-      <view class="action-buttons">
+
+
+
+      <!-- 复诊记录 -->
+      <view class="followup-section" v-show="followUps.length > 0">
+        <view class="section-title">我的复诊记录</view>
+        <view class="followup-list">
+          <view class="followup-card" v-for="followUp in followUps" :key="followUp.id">
+            <view class="card-header">
+              <view class="status-badge" :class="followUp.status">
+                {{ getFollowUpStatusText(followUp.status) }}
+              </view>
+            </view>
+
+            <view class="card-content">
+              <view class="info-row">
+                <text class="info-label">预约时间:</text>
+                <text class="info-value">{{ formatDate(followUp.appointmentTime) }}</text>
+              </view>
+
+              <view class="info-row" v-if="followUp.reason">
+                <text class="info-label">复诊原因:</text>
+                <text class="info-value reason-text">{{ followUp.reason }}</text>
+              </view>
+
+              <!-- 操作按钮:仅对PENDING和CONFIRMED状态显示 -->
+              <view class="action-row" v-if="followUp.status === 'PENDING' || followUp.status === 'CONFIRMED'">
+                <button class="action-btn secondary" @click="editFollowUp(followUp)">编辑</button>
+                <button class="action-btn cancel" @click="cancelFollowUp(followUp.id)">取消</button>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="action-buttons" v-if="!hasPendingOrConfirmedFollowUp">
         <button class="action-btn primary" @click="makeAppointment">预约复诊</button>
-        <button class="action-btn secondary" @click="contactDoctor">联系医生</button>
       </view>
     </view>
-    
+
     <view class="empty-state" v-else>
       <image class="empty-icon" src="/static/icons/remixicon/account-circle-line.svg" />
       <text class="empty-text">暂无绑定的医生</text>
@@ -31,11 +64,14 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
+import { ref, computed, onMounted } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
 import CustomNav from '@/components/custom-nav.vue'
 import { listUserBindingsByPatient, type UserBindingResponse, type UserBindingPageResponse } from '@/api/userBinding'
 import { downloadAvatar } from '@/api/user'
+import { getFollowUpList, updateFollowUp, deleteFollowUp } from '@/api/followUp'
+import type { FollowUp } from '@/api/followUp'
+import { formatDate } from '@/utils/date'
 
 // 简化医生信息接口,只包含API实际返回的字段
 interface LocalDoctorInfo {
@@ -54,6 +90,19 @@ const pageData = ref({
   pages: 0
 })
 
+// 复诊记录相关
+const followUps = ref<FollowUp[]>([])
+
+// 计算是否有待处理或已确认的复诊请求
+const hasPendingOrConfirmedFollowUp = computed(() => {
+  return followUps.value.some(followUp => 
+    followUp.status === 'PENDING' || followUp.status === 'CONFIRMED'
+  )
+})
+
+// 调试信息
+const debugInfo = ref('')
+
 const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
 
 // 下载后本地临时保存的图片路径(uni.downloadFile 会产出 tempFilePath)
@@ -73,7 +122,7 @@ const doctorAvatar = computed(() => {
 // 获取医生信息
 const fetchDoctorInfo = async () => {
   uni.showLoading({ title: '加载中...' })
-  
+
   try {
     const token = uni.getStorageSync('token')
     if (!token) {
@@ -84,11 +133,11 @@ const fetchDoctorInfo = async () => {
       })
       return
     }
-    
+
     // 获取当前用户ID
     const userInfo = uni.getStorageSync('user_info')
     const patientUserId = userInfo?.id
-    
+
     if (!patientUserId) {
       uni.hideLoading()
       uni.showToast({
@@ -97,38 +146,38 @@ const fetchDoctorInfo = async () => {
       })
       return
     }
-    
+
     // 查询患者绑定的医生列表
-    const response = await listUserBindingsByPatient(
-      patientUserId, 
-      'DOCTOR', 
+    const doctorResponse = await listUserBindingsByPatient(
+      patientUserId,
+      'DOCTOR',
       {
         pageNum: pageData.value.pageNum,
         pageSize: pageData.value.pageSize
       }
     )
-    
+
     uni.hideLoading()
-    
-    const resp = response.data as any
-    
+
+    const resp = doctorResponse.data as any
+
     if (resp && resp.code === 200 && resp.data) {
       const pageResult = resp.data as UserBindingPageResponse
       userBindings.value = pageResult.records
       pageData.value.total = pageResult.total
       pageData.value.pages = pageResult.pages
-      
+
       // 如果有绑定的医生,获取第一个医生的详细信息
       if (pageResult.records && pageResult.records.length > 0) {
         const boundDoctor = pageResult.records[0]
-        
+
         // 直接使用绑定接口返回的信息,不再调用额外的用户详情接口
         doctorInfo.value = {
           id: boundDoctor.id,
           name: boundDoctor.boundUserNickname || '未知医生',
           phone: boundDoctor.boundUserPhone || '未提供', // 当电话为null时显示"未提供"
         }
-        
+
         // 尝试下载头像(绑定接口返回的数据中可能没有 avatar)
         try {
           if (boundDoctor.boundUserId) {
@@ -159,30 +208,149 @@ const fetchDoctorInfo = async () => {
   }
 }
 
+// 获取复诊记录
+const fetchFollowUpRecords = async () => {
+  try {
+    const followUpResponse: any = await getFollowUpList({
+      pageNum: 1,
+      pageSize: 10
+    })
+
+    // 检查响应结构
+    if (!followUpResponse) {
+      return
+    }
+
+    if (!followUpResponse.data) {
+      return
+    }
+
+    // 注意:这里需要访问 followUpResponse.data.data 才是真正的数据
+    const apiResponse = followUpResponse.data
+
+    if (apiResponse.code !== 200) {
+      return
+    }
+
+    // 检查实际数据字段
+    const data = apiResponse.data
+
+    if (!data) {
+      return
+    }
+
+    if (!data.records) {
+      return
+    }
+
+    if (!Array.isArray(data.records)) {
+      return
+    }
+
+    // 正常处理records
+    followUps.value = data.records || []
+
+    // 隐藏加载提示(如果有)
+    uni.hideLoading()
+  } catch (error) {
+    console.error('获取复诊记录失败:', error)
+    uni.showToast({
+      title: '获取复诊记录失败',
+      icon: 'none'
+    })
+
+    // 隐藏加载提示(如果有)
+    uni.hideLoading()
+  }
+}
+
 const makeAppointment = () => {
+  // 跳转到复诊申请页面,传递医生信息
+  if (doctorInfo.value) {
+    uni.navigateTo({
+      url: `/pages/patient/profile/infos/followup-request?doctorId=${doctorInfo.value.id}&doctorName=${encodeURIComponent(doctorInfo.value.name)}&boundUserId=${userBindings.value[0]?.boundUserId || ''}`
+    })
+  } else {
+    uni.showToast({
+      title: '未获取到医生信息',
+      icon: 'none'
+    })
+  }
+}
+
+const bindDoctor = () => {
   uni.showToast({
-    title: '预约功能开发中',
+    title: '绑定医生功能开发中',
     icon: 'none'
   })
 }
 
-const contactDoctor = () => {
-  uni.showToast({
-    title: '联系医生功能开发中',
-    icon: 'none'
+// 编辑复诊记录
+const editFollowUp = (followUp: FollowUp) => {
+  // 跳转到复诊编辑页面,传递复诊记录信息
+  uni.navigateTo({
+    url: `/pages/patient/profile/infos/followup-edit?id=${followUp.id}&doctorId=${doctorInfo.value?.id}&doctorName=${encodeURIComponent(doctorInfo.value?.name || '')}&appointmentTime=${encodeURIComponent(followUp.appointmentTime)}&reason=${encodeURIComponent(followUp.reason || '')}&boundUserId=${userBindings.value[0]?.boundUserId || ''}`
   })
 }
 
-const bindDoctor = () => {
-  uni.showToast({
-    title: '绑定医生功能开发中',
-    icon: 'none'
+// 取消复诊记录
+const cancelFollowUp = (id: string) => {
+  uni.showModal({
+    title: '确认取消',
+    content: '确定要取消这个复诊预约吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 调用接口取消复诊记录
+        updateFollowUp(id, { status: 'CANCELLED' })
+          .then((res: any) => {
+            if (res && res.data && res.data.code === 200) {
+              uni.showToast({
+                title: '取消成功',
+                icon: 'success'
+              })
+              // 重新获取复诊记录列表
+              fetchFollowUpRecords()
+            } else {
+              uni.showToast({
+                title: '取消失败',
+                icon: 'none'
+              })
+            }
+          })
+          .catch((error) => {
+            console.error('取消复诊记录失败:', error)
+            uni.showToast({
+              title: '取消失败',
+              icon: 'none'
+            })
+          })
+      }
+    }
   })
 }
 
 onLoad(() => {
   fetchDoctorInfo()
+  fetchFollowUpRecords()
 })
+
+onShow(() => {
+  // 页面每次显示时都刷新复诊记录列表
+  // 这样从创建或编辑页面返回时能获取最新数据
+  fetchFollowUpRecords()
+})
+
+// 获取复诊状态文本
+const getFollowUpStatusText = (status: string) => {
+  switch (status) {
+    case 'PENDING': return '待处理'
+    case 'CONFIRMED': return '已确认'
+    case 'CANCELLED': return '已取消'
+    case 'COMPLETED': return '已完成'
+    default: return status
+  }
+}
+
 </script>
 
 <style scoped>
@@ -193,6 +361,7 @@ onLoad(() => {
   padding-bottom: 40rpx;
 }
 
+
 .doctor-card {
   background-color: #fff;
   margin: 20rpx;
@@ -296,4 +465,148 @@ onLoad(() => {
   line-height: 80rpx;
   width: 80%;
 }
+
+.followup-section {
+  margin: 0 20rpx 20rpx;
+  background-color: #fff;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  border: 1rpx solid #eee;
+}
+
+.section-title {
+  padding: 24rpx 30rpx;
+  font-size: 32rpx;
+  font-weight: 500;
+  color: #333;
+  border-bottom: 1rpx solid #eee;
+}
+
+.followup-list {
+  padding: 20rpx;
+}
+
+.followup-card {
+  background: #fff;
+  border-radius: 12rpx;
+  box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
+  margin-bottom: 20rpx;
+  overflow: hidden;
+  border: 1rpx solid #eee;
+}
+
+.followup-card:last-child {
+  margin-bottom: 0;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx 24rpx;
+  background-color: #f8f9fa;
+  border-bottom: 1rpx solid #eee;
+}
+
+.status-badge {
+  font-size: 24rpx;
+  padding: 6rpx 20rpx;
+  border-radius: 30rpx;
+  color: #fff;
+  font-weight: normal;
+}
+
+.status-badge.PENDING {
+  background-color: #ff9500;
+}
+
+.status-badge.CONFIRMED {
+  background-color: #007aff;
+}
+
+.status-badge.COMPLETED {
+  background-color: #34c759;
+}
+
+.status-badge.CANCELLED {
+  background-color: #8e8e93;
+}
+
+.appointment-time {
+  font-size: 24rpx;
+  color: #666;
+}
+
+.card-content {
+  padding: 24rpx;
+}
+
+.info-row {
+  display: flex;
+  margin-bottom: 16rpx;
+  align-items: flex-start;
+}
+
+.info-row:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  color: #888;
+  font-size: 26rpx;
+  width: 140rpx;
+  flex-shrink: 0;
+}
+
+.info-value {
+  flex: 1;
+  color: #333;
+  font-size: 26rpx;
+  line-height: 1.5;
+}
+
+.reason-text {
+  color: #555;
+}
+
+.action-row {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 20rpx;
+  padding-top: 20rpx;
+  border-top: 1rpx solid #eee;
+}
+
+.action-btn {
+  flex: 1;
+  border-radius: 10rpx;
+  font-size: 28rpx;
+  line-height: 70rpx;
+}
+
+.primary {
+  background-color: #3742fa;
+  color: #fff;
+}
+
+.secondary {
+  background-color: #f0f0f0;
+  color: #333;
+}
+
+.cancel {
+  background-color: #ff4757;
+  color: #fff;
+}
+
+.debug-info {
+  padding: 20rpx;
+  background-color: #ffeb3b;
+  color: #333;
+  font-size: 28rpx;
+  margin: 20rpx;
+  border-radius: 10rpx;
+  white-space: pre-wrap;
+}
 </style>

+ 212 - 0
src/pages/patient/profile/infos/followup-edit.vue

@@ -0,0 +1,212 @@
+<template>
+  <CustomNav title="编辑复诊预约" leftType="back" />
+  <view class="page-container">
+    <view class="form-container">
+      <view class="form-item">
+        <text class="form-label">预约医生</text>
+        <input class="form-input" disabled :value="doctorName" />
+      </view>
+      
+      <view class="form-item">
+        <text class="form-label">预约时间</text>
+        <picker mode="date" :value="formData.date" @change="onDateChange" start="2020-01-01" end="2030-12-31">
+          <view class="picker">
+            {{ formData.date || '请选择日期' }}
+          </view>
+        </picker>
+      </view>
+      
+      <view class="form-item">
+        <picker mode="time" :value="formData.time" @change="onTimeChange">
+          <view class="picker">
+            {{ formData.time || '请选择时间' }}
+          </view>
+        </picker>
+      </view>
+      
+      <view class="form-item">
+        <text class="form-label">复诊原因</text>
+        <textarea class="form-textarea" placeholder="请输入复诊原因(可选)" v-model="formData.reason" />
+      </view>
+      
+      <button class="submit-btn" @click="submitForm">保存修改</button>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import CustomNav from '@/components/custom-nav.vue'
+import { updateFollowUp } from '@/api/followUp'
+import { formatDate } from '@/utils/date'
+
+const doctorName = ref('')
+const doctorId = ref('')
+const followUpId = ref('')
+
+const formData = reactive({
+  date: '',
+  time: '',
+  reason: ''
+})
+
+onLoad((options) => {
+  if (options) {
+    // 获取传递的参数
+    doctorName.value = decodeURIComponent(options.doctorName || '')
+    doctorId.value = options.doctorId || ''
+    followUpId.value = options.id || ''
+    
+    // 解析预约时间
+    if (options.appointmentTime) {
+      const timeStr = decodeURIComponent(options.appointmentTime)
+      const dateObj = new Date(timeStr)
+      formData.date = formatDisplayDate(dateObj)
+      formData.time = `${String(dateObj.getHours()).padStart(2, '0')}:${String(dateObj.getMinutes()).padStart(2, '0')}`
+    }
+    
+    // 获取复诊原因
+    formData.reason = decodeURIComponent(options.reason || '')
+  }
+})
+
+const onDateChange = (e: any) => {
+  formData.date = e.detail.value
+}
+
+const onTimeChange = (e: any) => {
+  formData.time = e.detail.value
+}
+
+const formatDisplayDate = (d: Date) => {
+  const x = new Date(d)
+  const y = x.getFullYear()
+  const m = String(x.getMonth() + 1).padStart(2, '0')
+  const day = String(x.getDate()).padStart(2, '0')
+  return `${y}-${m}-${day}`
+}
+
+const submitForm = async () => {
+  if (!formData.date) {
+    uni.showToast({
+      title: '请选择预约日期',
+      icon: 'none'
+    })
+    return
+  }
+  
+  if (!formData.time) {
+    uni.showToast({
+      title: '请选择预约时间',
+      icon: 'none'
+    })
+    return
+  }
+  
+  // 构造完整的预约时间
+  const appointmentTime = `${formData.date}T${formData.time}:00`
+  
+  uni.showLoading({
+    title: '保存中...'
+  })
+  
+  try {
+    const res: any = await updateFollowUp(followUpId.value, {
+      appointmentTime,
+      reason: formData.reason || undefined
+    })
+    
+    uni.hideLoading()
+    
+    if (res && res.data && res.data.code === 200) {
+      uni.showToast({
+        title: '保存成功',
+        icon: 'success'
+      })
+      
+      // 返回上一页并刷新数据
+      setTimeout(() => {
+        uni.navigateBack()
+      }, 1000)
+    } else {
+      uni.showToast({
+        title: res.data?.message || '保存失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('编辑复诊预约失败:', error)
+    uni.showToast({
+      title: '保存失败',
+      icon: 'none'
+    })
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 40rpx;
+}
+
+.form-container {
+  padding: 20rpx;
+  background-color: #fff;
+  margin: 20rpx;
+  border-radius: 20rpx;
+}
+
+.form-item {
+  padding: 30rpx 0;
+  border-bottom: 1rpx solid #eee;
+}
+
+.form-item:last-child {
+  border-bottom: none;
+}
+
+.form-label {
+  display: block;
+  font-size: 30rpx;
+  color: #333;
+  margin-bottom: 20rpx;
+  font-weight: 500;
+}
+
+.form-input {
+  font-size: 28rpx;
+  color: #333;
+  padding: 10rpx 0;
+}
+
+.picker {
+  font-size: 28rpx;
+  color: #333;
+  padding: 10rpx 0;
+}
+
+.form-textarea {
+  width: 100%;
+  height: 150rpx;
+  font-size: 28rpx;
+  color: #333;
+  padding: 10rpx 0;
+  border: 1rpx solid #eee;
+  border-radius: 10rpx;
+  box-sizing: border-box;
+}
+
+.submit-btn {
+  background-color: #3742fa;
+  color: #fff;
+  border-radius: 10rpx;
+  font-size: 32rpx;
+  line-height: 80rpx;
+  margin-top: 50rpx;
+}
+</style>

+ 254 - 0
src/pages/patient/profile/infos/followup-request.vue

@@ -0,0 +1,254 @@
+<template>
+  <CustomNav title="申请复诊" leftType="back" />
+  <view class="page-container">
+    <view class="form-container">
+      <view class="form-item">
+        <text class="label">就诊医生</text>
+        <view class="value">{{ doctorInfo?.name || '暂无绑定医生' }}</view>
+      </view>
+      
+      <view class="form-item">
+        <text class="label">预约时间</text>
+        <picker mode="date" :value="scheduledDate" @change="onDateChange">
+          <view class="picker">
+            {{ scheduledDate ? formatDate(scheduledDate) : '请选择预约日期' }}
+          </view>
+        </picker>
+      </view>
+      
+      <view class="form-item">
+        <picker mode="time" :value="scheduledTime" @change="onTimeChange">
+          <view class="picker">
+            {{ scheduledTime || '请选择预约时间' }}
+          </view>
+        </picker>
+      </view>
+      
+      <view class="form-item textarea-item">
+        <text class="label">复诊原因</text>
+        <textarea 
+          placeholder="请输入复诊原因..." 
+          v-model="reason"
+          maxlength="200"
+        />
+        <text class="char-count">{{ reason.length }}/200</text>
+      </view>
+      
+      <button class="submit-btn" @click="submitRequest" :disabled="!canSubmit">
+        提交申请
+      </button>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import CustomNav from '@/components/custom-nav.vue'
+import { formatDate } from '@/utils/date'
+import { createFollowUp } from '@/api/followUp'
+import type { CreateFollowUpRequest } from '@/api/followUp'
+
+interface DoctorInfo {
+  id: string
+  name: string
+}
+
+const doctorInfo = ref<DoctorInfo | null>(null)
+const scheduledDate = ref('')
+const scheduledTime = ref('')
+const reason = ref('')
+
+// 是否可以提交
+const canSubmit = computed(() => {
+  return doctorInfo.value && scheduledDate.value && scheduledTime.value && reason.value.trim()
+})
+
+// 日期选择
+const onDateChange = (e: any) => {
+  scheduledDate.value = e.detail.value
+}
+
+// 时间选择
+const onTimeChange = (e: any) => {
+  scheduledTime.value = e.detail.value
+}
+
+// 页面加载时获取传递的参数
+onLoad((options) => {
+  if (options && options.doctorId && options.doctorName) {
+    doctorInfo.value = {
+      id: options.boundUserId || options.doctorId,  // 优先使用医生用户ID
+      name: decodeURIComponent(options.doctorName)
+    }
+  } else {
+    // 如果没有传递参数,尝试获取绑定的医生信息
+    fetchDoctorInfo()
+  }
+})
+
+// 获取绑定的医生信息
+const fetchDoctorInfo = () => {
+  // 这里应该调用实际的 API 获取绑定的医生信息
+  // 示例数据
+  doctorInfo.value = {
+    id: '1',
+    name: '张医生'
+  }
+}
+
+// 提交复诊申请
+const submitRequest = async () => {
+  if (!canSubmit.value) return
+  
+  uni.showLoading({ title: '提交中...' })
+  
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '未登录',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 获取当前患者ID
+    const userInfo = uni.getStorageSync('user_info')
+    const patientUserId = userInfo?.id
+    
+    if (!patientUserId) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '获取患者信息失败',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 构造预约时间,使用 ISO 8601 标准格式
+    const appointmentTime = new Date(`${scheduledDate.value}T${scheduledTime.value}:00`).toISOString()
+    
+    // 构造创建复诊请求的参数
+    const payload: CreateFollowUpRequest = {
+      doctorUserId: doctorInfo.value!.id,
+      appointmentTime,
+      reason: reason.value.trim()
+    }
+    
+    // 调用API提交复诊申请
+    const response: any = await createFollowUp(payload)
+    
+    uni.hideLoading()
+    
+    if (response && response.data && response.data.code === 200) {
+      uni.showModal({
+        title: '提交成功',
+        content: '复诊申请已提交,请等待医生确认',
+        showCancel: false,
+        confirmText: '知道了',
+        success: () => {
+          uni.navigateBack()
+        }
+      })
+    } else {
+      uni.showToast({
+        title: '提交失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('提交复诊申请失败:', error)
+    uni.showToast({
+      title: '提交失败',
+      icon: 'none'
+    })
+  }
+}
+
+onMounted(() => {
+  // 如果onLoad没有设置医生信息,再尝试获取
+  if (!doctorInfo.value) {
+    fetchDoctorInfo()
+  }
+})
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 40rpx;
+}
+
+.form-container {
+  padding: 20rpx;
+}
+
+.form-item {
+  background-color: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+}
+
+.label {
+  font-size: 32rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+  font-weight: bold;
+}
+
+.value {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.picker {
+  font-size: 28rpx;
+  color: #666;
+  padding: 20rpx 0;
+}
+
+.textarea-item {
+  position: relative;
+}
+
+.textarea-item textarea {
+  width: 100%;
+  height: 200rpx;
+  font-size: 28rpx;
+  color: #333;
+  border: 1rpx solid #eee;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  box-sizing: border-box;
+}
+
+.char-count {
+  position: absolute;
+  right: 20rpx;
+  bottom: 20rpx;
+  font-size: 24rpx;
+  color: #999;
+}
+
+.submit-btn {
+  background-color: #3742fa;
+  color: #fff;
+  border-radius: 10rpx;
+  font-size: 32rpx;
+  height: 80rpx;
+  line-height: 80rpx;
+  margin-top: 40rpx;
+}
+
+.submit-btn[disabled] {
+  background-color: #ccc;
+}
+</style>

+ 11 - 0
src/utils/date.ts

@@ -1,4 +1,15 @@
 // 日期工具集:统一周/月计算与格式化,所有页面共享
+export function formatDate(date: string | Date): string {
+  const d = new Date(date)
+  const year = d.getFullYear()
+  const month = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  const hours = String(d.getHours()).padStart(2, '0')
+  const minutes = String(d.getMinutes()).padStart(2, '0')
+  const seconds = String(d.getSeconds()).padStart(2, '0')
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
 export function setToLocalMidnight(d: Date) {
   const x = new Date(d)
   x.setHours(0, 0, 0, 0)