feat: 用药提醒功能 + 移除医生相关页面

- 后端新增 GET /api/medications/reminders 接口
- 前端任务卡片区显示真实用药提醒
- 移除 DoctorListPage/DoctorChatPage 路由
- 移除"找医生"面板按钮
- 医生端另做 Web 页面
This commit is contained in:
MingNian
2026-06-03 15:11:12 +08:00
parent 0e49b9a952
commit 07ddf2577a
12 changed files with 399 additions and 45 deletions

View File

@@ -18,11 +18,34 @@ class _HomePageState extends ConsumerState<HomePage> {
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<HomePage> {
}
List<Widget> _getTodayTasks(Map<String, dynamic> 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 = <Widget>[];
// 1. 数据摘要卡片(有今日指标数据时显示)
final summaryParts = <String>[];
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<Widget> _buildAbnormalRows(Map<String, dynamic> healthData) {
@@ -163,25 +264,98 @@ class _HomePageState extends ConsumerState<HomePage> {
return rows;
}
Widget _taskRow({required IconData icon, required String label, required String status, VoidCallback? onTap}) {
Widget _summaryCard(List<String> 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<String, dynamic> 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<HomePage> {
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<HomePage> {
case '录入血氧': _textCtrl.text = '血氧 ';
case '录入体重': _textCtrl.text = '体重 ';
case '用药管理': pushRoute(ref, 'medications');
case '找医生': pushRoute(ref, 'doctors');
case '本周计划': case '新建计划': pushRoute(ref, 'exercisePlan');
}
}