Files
AI-Health/health_app/lib/pages/medication/medication_checkin_page.dart
MingNian 16d1d3d305 feat: 用药打卡全面重构 - 按次追踪 + 独立打卡页 + 任务区汇总
- 后端用药提醒改为按次粒度(每个服药时间独立判断)
- 新增 per-dose 打卡/取消打卡 API
- 新增 MedicationCheckInPage 独立打卡页(每药每时间单独toggle)
- 用药列表删底部弹窗(编辑/停药),点药品直接进该药打卡页
- 任务区用药汇总为一行(过期待服已服)
- 支持隔天(EveryOtherDay)/每周频率判断
- 删历史对话功能(ConversationItem + conversationListProvider + UI)
2026-06-08 16:21:13 +08:00

143 lines
6.5 KiB
Dart
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.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/auth_provider.dart';
import '../../providers/data_providers.dart';
class MedicationCheckInPage extends ConsumerStatefulWidget {
final String? medId; // 可选指定药品null 显示全部
const MedicationCheckInPage({super.key, this.medId});
@override ConsumerState<MedicationCheckInPage> createState() => _MedicationCheckInPageState();
}
class _MedicationCheckInPageState extends ConsumerState<MedicationCheckInPage> {
Future<List<Map<String, dynamic>>>? _future;
@override void initState() { super.initState(); _load(); }
void _load() => setState(() { _future = ref.read(medicationReminderProvider.future); });
Future<void> _toggleDose(String medId, String time, bool taken) async {
try {
final api = ref.read(apiClientProvider);
if (taken) {
// 取消打卡:删掉对应 log
await api.delete('/api/medications/$medId/confirm-dose/$time');
} else {
// 打卡
await api.post('/api/medications/$medId/confirm-dose', data: {'scheduledTime': time, 'status': 'taken'});
}
ref.invalidate(medicationReminderProvider);
ref.invalidate(medicationListProvider);
_load();
} catch (_) {}
}
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FC),
appBar: AppBar(title: const Text('服药打卡'), centerTitle: true),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF8B9CF7)));
}
final reminders = snap.data ?? [];
if (reminders.isEmpty) {
return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.check_circle_outline, size: 64, color: Colors.grey[300]),
const SizedBox(height: 12),
const Text('今天所有用药已打卡', style: TextStyle(fontSize: 16, color: Color(0xFF999999))),
]),
);
}
// 如果指定了 medId只显示该药品
var filtered = reminders;
if (widget.medId != null) {
filtered = reminders.where((r) => r['id']?.toString() == widget.medId).toList();
}
// 按药品分组
final grouped = <String, List<Map<String, dynamic>>>{};
for (final r in filtered) {
final name = r['name']?.toString() ?? '药品';
grouped.putIfAbsent(name, () => []).add(r);
}
return RefreshIndicator(
onRefresh: () async => _load(),
child: ListView(
padding: const EdgeInsets.all(14),
children: grouped.entries.map((entry) {
final medName = entry.key;
final doses = entry.value;
final dosage = doses.first['dosage']?.toString() ?? '';
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))],
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(
width: 40, height: 40,
decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(10)),
child: const Center(child: Text('💊', style: TextStyle(fontSize: 20))),
),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(medName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
if (dosage.isNotEmpty) Text(dosage, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
]),
]),
const SizedBox(height: 14),
...doses.map((d) {
final time = d['scheduledTime']?.toString() ?? '';
final status = d['status']?.toString() ?? 'pending';
final isTaken = status == 'taken';
final medId = d['id']?.toString() ?? '';
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(children: [
Icon(
isTaken ? Icons.check_circle : Icons.radio_button_unchecked,
size: 20,
color: isTaken ? const Color(0xFF43A047) : const Color(0xFFCCCCCC),
),
const SizedBox(width: 10),
Text(time, style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w500,
color: isTaken ? const Color(0xFF999999) : const Color(0xFF333333),
)),
const Spacer(),
GestureDetector(
onTap: () => _toggleDose(medId, time, isTaken),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isTaken ? const Color(0xFFDCFCE7) : const Color(0xFF8B9CF7),
borderRadius: BorderRadius.circular(20),
),
child: Text(isTaken ? '已打卡' : '打卡',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600,
color: isTaken ? const Color(0xFF43A047) : Colors.white)),
),
),
]),
);
}),
]),
);
}).toList(),
),
);
},
),
);
}
}