feat: 用药提醒功能 + 移除医生相关页面
- 后端新增 GET /api/medications/reminders 接口 - 前端任务卡片区显示真实用药提醒 - 移除 DoctorListPage/DoctorChatPage 路由 - 移除"找医生"面板按钮 - 医生端另做 Web 页面
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user