Files
AI-Health/健康管家-日历页实施文档.md
MingNian c2399b952f refactor: 4层架构重构 + 饮食VLM接入 + 多项修复
- 后端: remaining_endpoints拆分为6个独立文件
- 后端: AI Agent Handler从ai_chat_endpoints抽取为7个独立处理器
- 后端: 食物识别prompt改为输出结构化JSON
- 前端: 饮食识别从Mock替换为真实VLM API调用
- 前端: 首页图片上传URL修复(/api/upload→/api/files/upload)
- 前端: 拍饮食按钮导航到独立DietCapturePage
- 前端: 删除无用agent_bar.dart
- 前端: 修复widget_test.dart过期属性名
- 前端: 恢复ServicePackageCard和详情页
- 新增6份实施文档(情况/问诊/报告/建档/日历/视觉统一)
2026-06-03 23:17:37 +08:00

253 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 健康日历 — 实施文档
## 一、现状
`remaining_pages.dart``HealthCalendarPage` 用固定规律模拟事件:
```dart
// 每周日、六 固定显示"用药",每 7 天显示"运动",每月 20 号显示"复查"
if (date.day == 5 || date.day == 12 || date.day == 19 || date.day == 26) events.add('medication');
```
日历格子只显示小圆点,不反映任何真实数据。
## 二、目标
日历每个日期格子上用小圆点标注当天的真实事件:
| 事件类型 | 圆点颜色 | 数据来源 |
|---------|---------|---------|
| 用药计划 | `Color(0xFF8B9CF7)` 紫 | `/api/medications` — TimeOfDay 数组 |
| 运动计划 | `Color(0xFF43A047)` 绿 | `/api/exercise-plans/current` — DayOfWeek |
| 复查随访 | `Color(0xFFF59E0B)` 橙 | 新增 `/api/followups`(或复用现有数据结构) |
| 健康数据记录 | `Color(0xFF4D96FF)` 蓝 | `/api/health-records?date=xxx` — 当天有记录 |
点击某天 → 弹出该日详情(当天记录列表 + 可快速跳转)。
## 三、后端改动
### 3.1 新增日历聚合端点
新建 `Endpoints/calendar_endpoints.cs`
```csharp
public static class CalendarEndpoints
{
public static void MapCalendarEndpoints(this WebApplication app)
{
app.MapGet("/api/calendar", async (
int year, int month,
HttpContext http, AppDbContext db, CancellationToken ct) =>
{
var userId = GetUserId(http);
var start = new DateOnly(year, month, 1);
var end = start.AddMonths(1);
// ① 用药:查询该月所有活跃用药的服药日期
var medications = await db.Medications
.Where(m => m.UserId == userId && m.IsActive)
.Select(m => new { m.Id, m.Name, m.Dosage, m.TimeOfDay })
.ToListAsync(ct);
// 每天都有服药计划Daily频率映射到每一天
// ② 运动:查该月覆盖的运动计划
var plans = await db.ExercisePlans
.Where(p => p.UserId == userId && p.WeekStartDate < end)
.Include(p => p.Items)
.ToListAsync(ct);
// ③ 健康数据:查该月有记录的日期
var healthDates = await db.HealthRecords
.Where(r => r.UserId == userId
&& r.RecordedAt >= start.ToDateTime(TimeOnly.MinValue)
&& r.RecordedAt < end.ToDateTime(TimeOnly.MinValue))
.Select(r => DateOnly.FromDateTime(r.RecordedAt))
.Distinct()
.ToListAsync(ct);
// ④ 复查随访
var followups = await db.FollowUps
.Where(f => f.UserId == userId
&& f.ScheduledAt >= start.ToDateTime(TimeOnly.MinValue)
&& f.ScheduledAt < end.ToDateTime(TimeOnly.MinValue))
.Select(f => new { f.Id, f.Title, f.Department,
Date = DateOnly.FromDateTime(f.ScheduledAt) })
.ToListAsync(ct);
// 按日期聚合
var calendar = new Dictionary<string, object>();
for (var d = start; d < end; d = d.AddDays(1))
{
var key = d.ToString("yyyy-MM-dd");
var events = new List<string>();
// 用药事件
if (medications.Any(m => m.TimeOfDay.Count > 0))
events.Add("medication");
// 运动事件
if (plans.Any(p => p.Items.Any(i => (int)d.DayOfWeek == i.DayOfWeek && !i.IsRestDay)))
events.Add("exercise");
// 复查事件
if (followups.Any(f => f.Date == d))
events.Add("followup");
// 健康数据
if (healthDates.Contains(d))
events.Add("health_record");
if (events.Count > 0)
calendar[key] = new { date = key, events };
}
return Results.Ok(new { code = 0, data = calendar.Values, message = (string?)null });
}).RequireAuthorization();
}
private static Guid GetUserId(HttpContext http) => ...;
}
```
`Program.cs` 注册:
```csharp
app.MapCalendarEndpoints();
```
### 3.2 新增每日详情端点
```csharp
app.MapGet("/api/calendar/{date}", async (
string date, HttpContext http, AppDbContext db, CancellationToken ct) =>
{
var userId = GetUserId(http);
var d = DateOnly.Parse(date);
var dt = d.ToDateTime(TimeOnly.MinValue);
// 当日健康记录
var records = await db.HealthRecords
.Where(r => r.UserId == userId && r.RecordedAt >= dt && r.RecordedAt < dt.AddDays(1))
.OrderByDescending(r => r.RecordedAt)
.Select(r => new { r.Id, Type = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit })
.ToListAsync(ct);
// 当日服药打卡
var medLogs = await db.MedicationLogs
.Where(l => l.UserId == userId && l.CreatedAt >= dt && l.CreatedAt < dt.AddDays(1))
.Select(l => new { l.Id, l.Status, MedName = l.Medication.Name })
.ToListAsync(ct);
// 当日随访
var followup = await db.FollowUps
.Where(f => f.UserId == userId && f.ScheduledAt >= dt && f.ScheduledAt < dt.AddDays(1))
.Select(f => new { f.Id, f.Title, f.Department, f.DoctorName, f.Notes })
.FirstOrDefaultAsync(ct);
return Results.Ok(new
{
code = 0,
data = new { records, medicationLogs = medLogs, followup },
message = (string?)null
});
}).RequireAuthorization();
```
## 四、前端改动
### 4.1 数据层
`HealthCalendarPage` 改为 `ConsumerStatefulWidget`
```dart
class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
DateTime _currentMonth = DateTime.now();
Map<String, List<String>> _events = {}; // "2026-06-03" -> ["medication", "exercise"]
@override void initState() {
super.initState();
_loadMonth();
}
Future<void> _loadMonth() async {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/calendar', queryParameters: {
'year': _currentMonth.year,
'month': _currentMonth.month,
});
final list = (res.data['data'] as List?) ?? [];
setState(() {
_events = { for (var item in list)
(item as Map)['date']: List<String>.from(item['events'] ?? [])
};
});
}
}
```
### 4.2 替换 _getEvents
```dart
// 旧(删除):
List<String> _getEvents(DateTime date) {
final events = <String>[];
if (date.day == 5 || date.day == 12 ...) events.add('medication');
...
}
// 新:
List<String> _getEvents(DateTime date) {
final key = '${date.year}-${date.month.toString().padLeft(2,'0')}-${date.day.toString().padLeft(2,'0')}';
return _events[key] ?? [];
}
```
### 4.3 点击日期 → 详情弹窗
```dart
void _onDayTap(int day) {
final date = DateTime(_currentMonth.year, _currentMonth.month, day);
final key = date.toIso8601String().substring(0, 10);
_showDayDetail(key);
}
void _showDayDetail(String date) async {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/calendar/$date');
final data = res.data['data'];
if (!mounted) return;
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => _DayDetailSheet(date: date, data: data),
);
}
```
### 4.4 DayDetailSheet 组件
```dart
class _DayDetailSheet extends StatelessWidget {
// 健康数据区:指标名 + 数值
// 用药区:药品名 + 打卡状态
// 复查区:医院/科室/备注
// 空状态:"当天暂无记录"
}
```
## 五、文件改动清单
```
后端:
新建: Endpoints/calendar_endpoints.cs (~90行)
修改: Program.cs (+ app.MapCalendarEndpoints(), 1行)
前端:
修改: pages/remaining_pages.dart (HealthCalendarPage 重写, ~100行改)
```
## 六、不做的事情
- 不替换 Flutter 日历包(继续用自定义 GridView
- 不做月视图周视图切换
- 不做日历同步到系统日历