From 07ddf2577aadb3c75a2bad85925094c784d8a359 Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Wed, 3 Jun 2026 15:11:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E8=8D=AF=E6=8F=90=E9=86=92?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20+=20=E7=A7=BB=E9=99=A4=E5=8C=BB=E7=94=9F?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 GET /api/medications/reminders 接口 - 前端任务卡片区显示真实用药提醒 - 移除 DoctorListPage/DoctorChatPage 路由 - 移除"找医生"面板按钮 - 医生端另做 Web 页面 --- .../Endpoints/remaining_endpoints.cs | 25 +++ health_app/lib/core/app_router.dart | 9 +- health_app/lib/pages/chart/trend_page.dart | 126 +++++++++-- .../consultation/consultation_pages.dart | 5 +- health_app/lib/pages/home/home_page.dart | 203 ++++++++++++++++-- .../pages/profile/profile_detail_page.dart | 2 +- .../lib/pages/profile/profile_page.dart | 3 +- health_app/lib/pages/remaining_pages.dart | 4 +- .../lib/pages/report/ai_analysis_page.dart | 6 +- health_app/lib/pages/report/report_pages.dart | 50 +++++ health_app/lib/providers/data_providers.dart | 5 + health_app/lib/services/health_service.dart | 6 + 12 files changed, 399 insertions(+), 45 deletions(-) diff --git a/backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs b/backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs index 9ff93cf..4c8416b 100644 --- a/backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/remaining_endpoints.cs @@ -101,6 +101,31 @@ public static class RemainingEndpoints await db.SaveChangesAsync(ct); return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null }); }); + + // 获取待提醒的用药 + group.MapGet("/reminders", async (HttpContext http, AppDbContext db, CancellationToken ct) => + { + var userId = GetUserId(http); + var now = TimeOnly.FromDateTime(DateTime.Now); + var windowEnd = now.AddHours(1); + var meds = await db.Medications + .Where(m => m.UserId == userId && m.IsActive && m.TimeOfDay != null) + .ToListAsync(ct); + var due = meds.Where(m => m.TimeOfDay!.Any(t => t >= now && t <= windowEnd)).ToList(); + + // 检查今天是否已打卡 + var today = DateOnly.FromDateTime(DateTime.Now); + var dueMeds = new List(); + foreach (var m in due) + { + var logged = await db.MedicationLogs.AnyAsync(l => + l.MedicationId == m.Id && l.CreatedAt >= today.ToDateTime(TimeOnly.MinValue) && l.Status == MedicationLogStatus.Taken, ct); + if (!logged) + dueMeds.Add(new { m.Id, m.Name, m.Dosage, m.TimeOfDay }); + } + + return Results.Ok(new { code = 0, data = dueMeds, message = (string?)null }); + }); } public static void MapReportEndpoints(this WebApplication app) diff --git a/health_app/lib/core/app_router.dart b/health_app/lib/core/app_router.dart index 9ac36aa..0785885 100644 --- a/health_app/lib/core/app_router.dart +++ b/health_app/lib/core/app_router.dart @@ -7,7 +7,6 @@ import '../pages/medication/medication_list_page.dart'; import '../pages/medication/medication_edit_page.dart'; import '../pages/report/report_pages.dart'; import '../pages/report/ai_analysis_page.dart'; -import '../pages/consultation/consultation_pages.dart'; import '../pages/settings/settings_pages.dart'; import '../pages/settings/notification_prefs_page.dart'; import '../pages/profile/profile_page.dart'; @@ -29,8 +28,6 @@ Widget buildPage(RouteInfo route) { return const HealthCalendarPage(); case 'medications': return const MedicationListPage(); - case 'medicationAdd': - return const MedicationEditPage(); case 'medicationEdit': return const MedicationEditPage(); case 'reports': @@ -39,10 +36,6 @@ Widget buildPage(RouteInfo route) { return ReportDetailPage(id: params['id']!); case 'aiAnalysis': return const AiAnalysisPage(); - case 'doctors': - return const DoctorListPage(); - case 'consultation': - return DoctorChatPage(id: params['id']!); case 'exercisePlan': return const ExercisePlanPage(); case 'dietRecords': @@ -53,6 +46,8 @@ Widget buildPage(RouteInfo route) { return const ProfilePage(); case 'profileEdit': return const ProfileDetailPage(); + case 'editProfile': + return const EditProfilePage(); case 'devices': return const DeviceManagementPage(); case 'healthArchive': diff --git a/health_app/lib/pages/chart/trend_page.dart b/health_app/lib/pages/chart/trend_page.dart index 3fbdb26..39e1db6 100644 --- a/health_app/lib/pages/chart/trend_page.dart +++ b/health_app/lib/pages/chart/trend_page.dart @@ -15,6 +15,7 @@ class _TrendPageState extends ConsumerState { int _period = 7; bool _showAllRecords = false; late List> _data; + final _chartKey = GlobalKey(); static const _labels = { 'blood_pressure': '血压趋势', @@ -420,20 +421,24 @@ class _TrendPageState extends ConsumerState { ], ), const SizedBox(height: 12), - SizedBox( - height: 200, - child: CustomPaint( - painter: _TrendChartPainter( - data: _data, - metricType: widget.metricType, - isDualLine: _isDualLine, - yMin: yRange.min, - yMax: yRange.max, - yStep: yRange.step, - formatDateLabel: _formatDateLabel, - formatValue: _formatValue, + GestureDetector( + key: _chartKey, + onTapDown: (details) => _onChartTap(details, yRange), + child: SizedBox( + height: 200, + child: CustomPaint( + painter: _TrendChartPainter( + data: _data, + metricType: widget.metricType, + isDualLine: _isDualLine, + yMin: yRange.min, + yMax: yRange.max, + yStep: yRange.step, + formatDateLabel: _formatDateLabel, + formatValue: _formatValue, + ), + size: Size.infinite, ), - size: Size.infinite, ), ), ], @@ -570,6 +575,101 @@ class _TrendPageState extends ConsumerState { ), ); } + + // ==================== 图表点击检测 ==================== + void _onChartTap(TapDownDetails details, ({double min, double max, double step}) yRange) { + final renderBox = _chartKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null || _data.length < 2) return; + + final localPosition = renderBox.globalToLocal(details.globalPosition); + + const leftPadding = 44.0; + const rightPadding = 8.0; + final chartW = renderBox.size.width - leftPadding - rightPadding; + + // 找到 x 方向最近的数据点 + int nearestIndex = 0; + double minDist = double.infinity; + for (int i = 0; i < _data.length; i++) { + final pointX = leftPadding + (chartW * i / (_data.length - 1)); + final dist = (localPosition.dx - pointX).abs(); + if (dist < minDist) { + minDist = dist; + nearestIndex = i; + } + } + + // 点击偏离数据点太远则不响应 + if (minDist > 40) return; + + final item = _data[nearestIndex]; + final date = item['date'] as DateTime; + final val = item['value'] as num; + final val2 = item['value2'] as num?; + final status = _getStatus(val, value2: val2); + + String displayValue; + if (_isDualLine) { + displayValue = '${_formatValue(val)} / ${_formatValue(val2 ?? 0)} ${_getUnit()}'; + } else { + displayValue = '${_formatValue(val)} ${_getUnit()}'; + } + + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + _formatDateTime(date), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayValue, + style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF333333)), + ), + const SizedBox(height: 16), + Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: _getStatusColor(status), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: _getStatusColor(status).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + status, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _getStatusColor(status), + ), + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('关闭', style: TextStyle(color: Color(0xFF635BFF))), + ), + ], + ), + ); + } } // ============================================================ diff --git a/health_app/lib/pages/consultation/consultation_pages.dart b/health_app/lib/pages/consultation/consultation_pages.dart index cdfe668..2fcb3b9 100644 --- a/health_app/lib/pages/consultation/consultation_pages.dart +++ b/health_app/lib/pages/consultation/consultation_pages.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/navigation_provider.dart'; import '../../providers/data_providers.dart'; /// 医生列表页 @@ -59,9 +60,7 @@ class DoctorListPage extends ConsumerWidget { ), ), ElevatedButton( - onPressed: () async { - // TODO: 点击「咨询」创建问诊并跳转聊天页 - }, + onPressed: () => pushRoute(ref, 'consultation', params: {'id': d['id']?.toString() ?? ''}), child: const Text('咨询'), ), ]), diff --git a/health_app/lib/pages/home/home_page.dart b/health_app/lib/pages/home/home_page.dart index ee4fe37..08880a1 100644 --- a/health_app/lib/pages/home/home_page.dart +++ b/health_app/lib/pages/home/home_page.dart @@ -18,11 +18,34 @@ class _HomePageState extends ConsumerState { final _textCtrl = TextEditingController(); final _scrollCtrl = ScrollController(); bool _taskCardsExpanded = true; + double _lastScrollOffset = 0; + DateTime? _lastCollapseTime; + bool _medicationTaken = false; + bool _exerciseDone = false; + + static final _mockFollowUps = [ + {'hospital': '协和医院', 'department': '心内科', 'date': DateTime.now().add(const Duration(days: 2)), 'type': '复查'}, + {'hospital': '人民医院', 'department': '心内科', 'date': DateTime.now().add(const Duration(days: 3)), 'type': '复诊'}, + ]; @override void initState() { super.initState(); _scrollCtrl.addListener(_onScroll); } @override void dispose() { _textCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } - void _onScroll() {} + void _onScroll() { + if (!_scrollCtrl.hasClients) return; + final offset = _scrollCtrl.offset; + if (offset < _lastScrollOffset && _taskCardsExpanded) { + final delta = _lastScrollOffset - offset; + if (delta > 50) { + final now = DateTime.now(); + if (_lastCollapseTime == null || now.difference(_lastCollapseTime!) > const Duration(seconds: 2)) { + _lastCollapseTime = now; + setState(() => _taskCardsExpanded = false); + } + } + } + _lastScrollOffset = offset; + } void _sendMessage() { final text = _textCtrl.text.trim(); @@ -148,12 +171,90 @@ class _HomePageState extends ConsumerState { } List _getTodayTasks(Map healthData) { - return [ - _taskRow(icon: Icons.medication_rounded, label: '计划 8:00 吃 阿司匹林 100mg', status: 'done', onTap: _handleMedicationCheck), - _taskRow(icon: Icons.directions_run, label: '今日待运动:散步 30 分钟', status: 'pending', onTap: null), - _taskRow(icon: Icons.today, label: '今日测量:血压', status: 'pending', onTap: () => _textCtrl.text = '血压 '), - ..._buildAbnormalRows(healthData), - ]; + final now = DateTime.now(); + final tasks = []; + + // 1. 数据摘要卡片(有今日指标数据时显示) + final summaryParts = []; + final bp = healthData['BloodPressure']; + if (bp is Map) { + final s = bp['systolic']; + final d = bp['diastolic']; + if (s != null && d != null) summaryParts.add('血压 $s/$d'); + } + final hr = healthData['HeartRate']; + if (hr is int) summaryParts.add('心率 $hr'); + final bs = healthData['BloodSugar']; + if (bs is num) summaryParts.add('血糖 $bs'); + final bo = healthData['BloodOxygen']; + if (bo is num) summaryParts.add('血氧 $bo'); + final wt = healthData['Weight']; + if (wt is num) summaryParts.add('体重 $wt'); + + if (summaryParts.isNotEmpty) { + tasks.add(_summaryCard(summaryParts)); + } + + // 2. 用药提醒(从后端拉取真实数据) + final reminders = ref.watch(medicationReminderProvider); + reminders.whenData((meds) { + for (final m in meds) { + final name = m['name'] ?? ''; + final dosage = m['dosage'] ?? ''; + final times = (m['timeOfDay'] as List?)?.map((t) => t.toString().substring(0, 5)).join(', ') ?? ''; + final medOverdue = now.hour >= 8; + tasks.add(_taskRow( + icon: Icons.medication_rounded, + label: '$name $dosage ($times)', + status: 'pending', + isOverdue: medOverdue, + onTap: () => _handleMedicationCheck, + )); + } + }); + + // 无提醒时显示默认用药卡片 + if (tasks.length <= (summaryParts.isNotEmpty ? 1 : 0)) { + tasks.add(_taskRow( + icon: Icons.medication_rounded, + label: '暂无用药提醒', + status: 'pending', + onTap: null, + )); + } + + // 3. 运动卡片(超时变红) + final exOverdue = now.hour >= 18 && !_exerciseDone; + tasks.add(_taskRow( + icon: Icons.directions_run, + label: '今日待运动:散步 30 分钟', + status: _exerciseDone ? 'done' : 'pending', + isOverdue: exOverdue, + onTap: _exerciseDone ? null : () => setState(() => _exerciseDone = true), + )); + + // 4. 测量卡片 + tasks.add(_taskRow( + icon: Icons.today, + label: '今日测量:血压', + status: 'pending', + onTap: () => _textCtrl.text = '血压 ', + )); + + // 5. 异常指标 + tasks.addAll(_buildAbnormalRows(healthData)); + + // 6. 复查提醒(未来3天内有复查安排时显示) + final upcomingFollowUps = _mockFollowUps.where((f) { + final date = f['date'] as DateTime; + return date.difference(now).inDays <= 3 && date.isAfter(now); + }).toList(); + + if (upcomingFollowUps.isNotEmpty) { + tasks.add(_followUpCard(upcomingFollowUps.first)); + } + + return tasks; } List _buildAbnormalRows(Map healthData) { @@ -163,25 +264,98 @@ class _HomePageState extends ConsumerState { return rows; } - Widget _taskRow({required IconData icon, required String label, required String status, VoidCallback? onTap}) { + Widget _summaryCard(List parts) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => pushRoute(ref, 'trend'), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFFF1F8E9), + borderRadius: BorderRadius.circular(10), + ), + child: Row(children: [ + const Icon(Icons.check_circle, size: 18, color: Color(0xFF43A047)), + const SizedBox(width: 10), + Expanded(child: Text( + '今日已记录:${parts.join('、')}', + style: const TextStyle(fontSize: 13, color: Color(0xFF333333)), + )), + ]), + ), + ), + ); + } + + Widget _followUpCard(Map followUp) { + final date = followUp['date'] as DateTime; + final now = DateTime.now(); + final diff = date.difference(now).inDays; + String dateLabel; + if (diff == 0) { + dateLabel = '今天'; + } else if (diff == 1) { + dateLabel = '明天'; + } else if (diff == 2) { + dateLabel = '后天'; + } else { + dateLabel = '$diff天后'; + } + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => pushRoute(ref, 'followups'), + child: Row(children: [ + Container( + width: 30, height: 30, + decoration: BoxDecoration( + color: const Color(0xFFF5F3FF), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.event_available, size: 15, color: Color(0xFF635BFF)), + ), + const SizedBox(width: 10), + Expanded(child: Text( + '📋 $dateLabel ${followUp['hospital']} ${followUp['department']} ${followUp['type']}', + style: const TextStyle(fontSize: 13, color: Color(0xFF333333)), + )), + ]), + ), + ); + } + + Widget _taskRow({required IconData icon, required String label, required String status, VoidCallback? onTap, bool isOverdue = false}) { final colors = {'done': const Color(0xFF43A047), 'warning': const Color(0xFFFF9800), 'pending': const Color(0xFF9E9E9E)}; final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined}; + final effectiveStatus = isOverdue ? 'warning' : status; return Padding( padding: const EdgeInsets.only(bottom: 10), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, - child: Row(children: [ - Container(width: 30, height: 30, decoration: BoxDecoration(color: const Color(0xFFF5F3FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 15, color: const Color(0xFF635BFF))), - const SizedBox(width: 10), Expanded(child: Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF333333)))), - Icon(icons[status], size: 18, color: colors[status] ?? Colors.grey), - ]), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: isOverdue ? const Color(0xFFFFEBEE) : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Row(children: [ + Container(width: 30, height: 30, decoration: BoxDecoration(color: const Color(0xFFF5F3FF), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 15, color: const Color(0xFF635BFF))), + const SizedBox(width: 10), Expanded(child: Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF333333)))), + Icon(icons[effectiveStatus], size: 18, color: colors[effectiveStatus] ?? Colors.grey), + ]), + ), ), ); } void _handleMedicationCheck() async { await ref.read(medicationServiceProvider).confirm(''); + setState(() => _medicationTaken = true); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已记录服药 ✅'), backgroundColor: Color(0xFF635BFF))); } @@ -260,7 +434,7 @@ class _HomePageState extends ConsumerState { case ActiveAgent.health: return [_agentBtn('录入血压', Icons.favorite), _agentBtn('录入血糖', Icons.bloodtype), _agentBtn('录入心率', Icons.monitor_heart), _agentBtn('录入血氧', Icons.air), _agentBtn('录入体重', Icons.monitor_weight)]; case ActiveAgent.diet: return [_agentBtn('拍照识别', Icons.camera_alt), _agentBtn('上传照片', Icons.photo_library)]; case ActiveAgent.medication: return [_agentBtn('用药管理', Icons.medication), _agentBtn('用药提醒', Icons.alarm)]; - case ActiveAgent.consultation: return [_agentBtn('找医生', Icons.person_search)]; + case ActiveAgent.consultation: return []; case ActiveAgent.exercise: return [_agentBtn('本周计划', Icons.calendar_view_week), _agentBtn('新建计划', Icons.add_circle_outline)]; default: return []; } @@ -287,7 +461,6 @@ class _HomePageState extends ConsumerState { case '录入血氧': _textCtrl.text = '血氧 '; case '录入体重': _textCtrl.text = '体重 '; case '用药管理': pushRoute(ref, 'medications'); - case '找医生': pushRoute(ref, 'doctors'); case '本周计划': case '新建计划': pushRoute(ref, 'exercisePlan'); } } diff --git a/health_app/lib/pages/profile/profile_detail_page.dart b/health_app/lib/pages/profile/profile_detail_page.dart index 36cde32..fba87c9 100644 --- a/health_app/lib/pages/profile/profile_detail_page.dart +++ b/health_app/lib/pages/profile/profile_detail_page.dart @@ -58,7 +58,7 @@ class ProfileDetailPage extends ConsumerWidget { Widget _buildHistoryList() { final items = [{'date': '05-31', 'label': '血压 · 餐前', 'value': '128/82', 'status': 'normal'}, {'date': '05-30', 'label': '血压 · 餐后', 'value': '135/85', 'status': 'warning'}, {'date': '05-29', 'label': '血压 · 餐前', 'value': '122/78', 'status': 'normal'}, {'date': '05-28', 'label': '血压 · 餐前', 'value': '118/76', 'status': 'normal'}, {'date': '05-27', 'label': '血糖 · 空腹', 'value': '5.6', 'status': 'normal'}, {'date': '05-26', 'label': '血压 · 餐前', 'value': '120/80', 'status': 'normal'}]; - return Container(decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]), child: Column(children: [Container(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row(children: [const Text('历史记录', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), Text('查看更多', style: TextStyle(fontSize: 13, color: const Color(0xFF635BFF)))])), ...items.map((item) => _historyItem(item)).toList()])); + return Container(decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]), child: Column(children: [Container(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row(children: [const Text('历史记录', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), Text('查看更多', style: TextStyle(fontSize: 13, color: const Color(0xFF635BFF)))])), ...items.map(_historyItem)])); } Widget _historyItem(Map item) { diff --git a/health_app/lib/pages/profile/profile_page.dart b/health_app/lib/pages/profile/profile_page.dart index fb022bb..a539e40 100644 --- a/health_app/lib/pages/profile/profile_page.dart +++ b/health_app/lib/pages/profile/profile_page.dart @@ -18,7 +18,7 @@ class ProfilePage extends ConsumerWidget { const SizedBox(height: 20), Row(children: [ GestureDetector( - onTap: () => pushRoute(ref, 'profileEdit'), + onTap: () => pushRoute(ref, 'editProfile'), child: Stack(children: [ CircleAvatar(radius: 32, backgroundColor: const Color(0xFFEDEBFF), backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, child: user?.avatarUrl == null ? const Icon(Icons.person, size: 40, color: Color(0xFF635BFF)) : null), Positioned(right: 0, bottom: 0, child: Container(width: 22, height: 22, decoration: BoxDecoration(color: const Color(0xFF635BFF), borderRadius: BorderRadius.circular(11), border: Border.all(color: Colors.white, width: 2)), child: const Icon(Icons.edit, size: 12, color: Colors.white))), @@ -37,7 +37,6 @@ class ProfilePage extends ConsumerWidget { _MenuItem(icon: Icons.folder_shared, title: '健康档案', onTap: () => pushRoute(ref, 'healthArchive')), _MenuItem(icon: Icons.devices, title: '设备管理', onTap: () => pushRoute(ref, 'devices')), _MenuItem(icon: Icons.favorite_border, title: '就诊收藏', trailing: '3'), - _MenuItem(icon: Icons.devices, title: '设备管理'), _MenuItem(icon: Icons.people_outline, title: '家人关怀'), _MenuItem(icon: Icons.local_hospital_outlined, title: '医生绑定记录'), _MenuItem(icon: Icons.chat_bubble_outline, title: '意见反馈'), diff --git a/health_app/lib/pages/remaining_pages.dart b/health_app/lib/pages/remaining_pages.dart index 456d850..150f917 100644 --- a/health_app/lib/pages/remaining_pages.dart +++ b/health_app/lib/pages/remaining_pages.dart @@ -148,6 +148,7 @@ class ExercisePlanPage extends ConsumerWidget { 'items': items, }); ref.invalidate(currentExercisePlanProvider); + if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('运动计划已创建 ✅'), backgroundColor: Color(0xFF635BFF), @@ -158,6 +159,7 @@ class ExercisePlanPage extends ConsumerWidget { final service = ref.read(exerciseServiceProvider); await service.checkIn(itemId); ref.invalidate(currentExercisePlanProvider); + if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('打卡成功 ✅'), backgroundColor: Color(0xFF43A047), @@ -222,7 +224,7 @@ class _ExercisePlanItem extends StatelessWidget { ]), const SizedBox(height: 4), Text( - isRest ? '休息日,好好休息' : '$exerciseType ${duration}分钟', + isRest ? '休息日,好好休息' : '$exerciseType $duration分钟', style: TextStyle(fontSize: 14, color: Colors.grey[500]), ), ]), diff --git a/health_app/lib/pages/report/ai_analysis_page.dart b/health_app/lib/pages/report/ai_analysis_page.dart index 76638c3..8c7fa8d 100644 --- a/health_app/lib/pages/report/ai_analysis_page.dart +++ b/health_app/lib/pages/report/ai_analysis_page.dart @@ -23,7 +23,7 @@ class AiAnalysisPage extends ConsumerWidget { Widget _buildIndicators() { final indicators = [{'name': '红细胞 (RBC)', 'value': '4.68', 'unit': '(×10¹²/L)', 'ref': '4.0-5.50', 'status': 'normal'}, {'name': '白细胞 (WBC)', 'value': '6.55', 'unit': '(×10⁹/L)', 'ref': '3.5-9.50', 'status': 'normal'}, {'name': '血红蛋白 (HGB)', 'value': '135', 'unit': '(g/L)', 'ref': '120-175', 'status': 'normal'}, {'name': '血小板 (PLT)', 'value': '235', 'unit': '(×10⁹/L)', 'ref': '125-350', 'status': 'normal'}]; - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [const Text('指标详情', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 12), ...indicators.map((item) => _indicatorCard(item)).toList()]); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [const Text('指标详情', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const SizedBox(height: 12), ...indicators.map((item) => _indicatorCard(item))]); } Widget _indicatorCard(Map item) { @@ -31,11 +31,11 @@ class AiAnalysisPage extends ConsumerWidget { return Container(margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: isNormal ? const Color(0xFFF8FDFB) : const Color(0xFFFFF8F5), borderRadius: BorderRadius.circular(14), border: Border.all(color: isNormal ? const Color(0xFFD4EDDA) : const Color(0xFFFFD7C5))), child: Row(children: [Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(item['name']?.toString() ?? '', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF333333))), const SizedBox(height: 4), Text('参考范围:${item['ref']?.toString() ?? ''}', style: TextStyle(fontSize: 12, color: Colors.grey[500]))])), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [Text('${item['value']?.toString() ?? ''} ${item['unit']?.toString() ?? ''}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))), const SizedBox(height: 2), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: isNormal ? const Color(0xFF43A047).withAlpha(20) : const Color(0xFFFF9800).withAlpha(20), borderRadius: BorderRadius.circular(8)), child: Text(isNormal ? '正常' : '偏高', style: TextStyle(fontSize: 11, color: isNormal ? const Color(0xFF43A047) : const Color(0xFFFF9800))))])])); } - Widget _buildAiInterpretation() => Container(width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: const Color(0xFFF5F3FF), borderRadius: BorderRadius.circular(16)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Row(children: [Icon(Icons.auto_awesome, size: 18, color: const Color(0xFF635BFF)), const SizedBox(width: 6), const Text('AI 智能解读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: const Color(0xFF635BFF).withAlpha(15), borderRadius: BorderRadius.circular(10)), child: const Text('已分析', style: TextStyle(fontSize: 11, color: Color(0xFF635BFF))))]), const SizedBox(height: 12), const Text('您的血常规检查结果基本正常,各项指标均在参考范围内。红细胞、白细胞、血小板计数均处于健康水平,血红蛋白含量充足,说明您的造血功能和免疫功能良好。建议继续保持良好的生活习惯,定期复查。', style: TextStyle(fontSize: 14, height: 1.6, color: const Color(0xFF444444)))])); + Widget _buildAiInterpretation() => Container(width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: const Color(0xFFF5F3FF), borderRadius: BorderRadius.circular(16)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Row(children: [Icon(Icons.auto_awesome, size: 18, color: const Color(0xFF635BFF)), const SizedBox(width: 6), Text('AI 智能解读', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))), const Spacer(), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: const Color(0xFF635BFF).withAlpha(15), borderRadius: BorderRadius.circular(10)), child: const Text('已分析', style: TextStyle(fontSize: 11, color: Color(0xFF635BFF))))]), const SizedBox(height: 12), const Text('您的血常规检查结果基本正常,各项指标均在参考范围内。红细胞、白细胞、血小板计数均处于健康水平,血红蛋白含量充足,说明您的造血功能和免疫功能良好。建议继续保持良好的生活习惯,定期复查。', style: TextStyle(fontSize: 14, height: 1.6, color: Color(0xFF444444)))])); Widget _buildDoctorAdvice() => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Row(children: [CircleAvatar(radius: 16, backgroundColor: const Color(0xFFEDEBFF), child: const Icon(Icons.local_hospital, size: 16, color: Color(0xFF635BFF))), const SizedBox(width: 8), const Text('医生建议', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))]), const SizedBox(height: 12), Container(width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFEEEEEE))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [_adviceItem('李医生', '心内科', '各项指标正常,继续保持。注意低盐饮食,适当运动。'), const Divider(), _adviceItem('王医生', '全科', '血常规结果理想,无需特殊处理。下次体检可关注血脂指标.')]))]); Widget _adviceItem(String name, String dept, String advice) => Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [CircleAvatar(radius: 14, backgroundColor: const Color(0xFFF5F3FF), child: Text(name[0], style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF635BFF)))), const SizedBox(width: 10), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Row(children: [Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF333333))), const SizedBox(width: 6), Text(dept, style: TextStyle(fontSize: 12, color: Colors.grey[500]))]), const SizedBox(height: 4), Text(advice, style: TextStyle(fontSize: 13, color: Colors.grey[700], height: 1.4))]))])); - Widget _buildHealthTips() => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Row(children: [Icon(Icons.lightbulb_outline, size: 18, color: const Color(0xFFFFB800)), const SizedBox(width: 8), const Text('健康提示', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))]), const SizedBox(height: 12), ...['定期进行血常规检查,建议每半年一次', '保持均衡饮食,多吃富含铁和维生素的食物', '适度运动,每周至少150分钟中等强度有氧运动', '保证充足睡眠,每晚7-8小时'].map((tip) => Padding(padding: const EdgeInsets.only(bottom: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Container(margin: const EdgeInsets.only(top: 6), width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFFFB800), shape: BoxShape.circle)), const SizedBox(width: 10), Expanded(child: Text(tip, style: TextStyle(fontSize: 14, color: Colors.grey[700], height: 1.4)))]))).toList()]); + Widget _buildHealthTips() => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Row(children: [Icon(Icons.lightbulb_outline, size: 18, color: const Color(0xFFFFB800)), const SizedBox(width: 8), const Text('健康提示', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))]), const SizedBox(height: 12), ...['定期进行血常规检查,建议每半年一次', '保持均衡饮食,多吃富含铁和维生素的食物', '适度运动,每周至少150分钟中等强度有氧运动', '保证充足睡眠,每晚7-8小时'].map((tip) => Padding(padding: const EdgeInsets.only(bottom: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [Container(margin: const EdgeInsets.only(top: 6), width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFFFB800), shape: BoxShape.circle)), const SizedBox(width: 10), Expanded(child: Text(tip, style: TextStyle(fontSize: 14, color: Colors.grey[700], height: 1.4)))])))]); } diff --git a/health_app/lib/pages/report/report_pages.dart b/health_app/lib/pages/report/report_pages.dart index fce1b88..43818c5 100644 --- a/health_app/lib/pages/report/report_pages.dart +++ b/health_app/lib/pages/report/report_pages.dart @@ -369,6 +369,35 @@ class ReportDetailPage extends ConsumerWidget { _buildAnalysisSection(analysis), const SizedBox(height: 20), _buildSummarySection(analysis), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 48, + child: OutlinedButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('图片加载中...'), duration: Duration(seconds: 2)), + ); + }, + icon: const Icon(Icons.image), + label: const Text('查看原始图片'), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF635BFF), + side: const BorderSide(color: Color(0xFF635BFF)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: () => pushRoute(ref, 'aiAnalysis'), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF635BFF), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24))), + child: const Text('查看 AI 智能解读'), + ), + ), const SizedBox(height: 30), ]), ), @@ -412,6 +441,27 @@ class ReportDetailPage extends ConsumerWidget { const Text('指标分析', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ]), ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF3E0), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFFFE0B2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outline, size: 16, color: Color(0xFFE65100)), + const SizedBox(width: 6), + const Text( + 'AI 预解读 · 待医生确认', + style: TextStyle(fontSize: 13, color: Color(0xFFE65100), fontWeight: FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 12), ...analysis.indicators.map((ind) => _buildIndicatorRow(ind)), ]), ); diff --git a/health_app/lib/providers/data_providers.dart b/health_app/lib/providers/data_providers.dart index c05e446..02eb6e6 100644 --- a/health_app/lib/providers/data_providers.dart +++ b/health_app/lib/providers/data_providers.dart @@ -45,6 +45,11 @@ final medicationListProvider = FutureProvider>>((ref) return service.getList(); }); +final medicationReminderProvider = FutureProvider>>((ref) async { + final service = ref.watch(medicationServiceProvider); + return service.getReminders(); +}); + /// 医生列表 Provider final doctorListProvider = FutureProvider>>((ref) async { final service = ref.watch(consultationServiceProvider); diff --git a/health_app/lib/services/health_service.dart b/health_app/lib/services/health_service.dart index ece40f4..5ea15df 100644 --- a/health_app/lib/services/health_service.dart +++ b/health_app/lib/services/health_service.dart @@ -83,6 +83,12 @@ class MedicationService { Future confirm(String id) async { await _api.post('/api/medications/$id/confirm'); } + + Future>> getReminders() async { + final res = await _api.get('/api/medications/reminders'); + final list = res.data['data'] as List? ?? []; + return list.cast>(); + } } /// 饮食服务