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

8.1 KiB
Raw Blame History

健康日历 — 实施文档

一、现状

remaining_pages.dartHealthCalendarPage 用固定规律模拟事件:

// 每周日、六 固定显示"用药",每 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

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 注册:

app.MapCalendarEndpoints();

3.2 新增每日详情端点

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

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

// 旧(删除):
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 点击日期 → 详情弹窗

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 组件

class _DayDetailSheet extends StatelessWidget {
    // 健康数据区:指标名 + 数值
    // 用药区:药品名 + 打卡状态
    // 复查区:医院/科室/备注
    // 空状态:"当天暂无记录"
}

五、文件改动清单

后端:
  新建: Endpoints/calendar_endpoints.cs   (~90行)
  修改: Program.cs                        (+ app.MapCalendarEndpoints(), 1行)

前端:
  修改: pages/remaining_pages.dart         (HealthCalendarPage 重写, ~100行改)

六、不做的事情

  • 不替换 Flutter 日历包(继续用自定义 GridView
  • 不做月视图周视图切换
  • 不做日历同步到系统日历