fix: 全面修复 - 实时通讯、报告AI流程、健康概览、用药运动提醒

- SignalR Hub消息持久化+防重复+跨端广播
- 报告VLM+LLM真实AI流程(验证→提取→解读)
- 医生审核页重构(严重程度/建议模板/综合评语)
- 健康概览五合一趋势图(fl_chart+指标切换)
- 用药提醒时区修复+跨午夜+过期提醒
- 运动打卡DayOfWeek映射修复+计划覆盖查询
- 体重与其他四指标同级(Abnormal检查/确认卡牌)
- AI Agent支持多种类多时段批量录入
- 删除硬编码假数据(张三/假医生/假AI解读)
- 随访/报告审核数据链路全部打通
This commit is contained in:
MingNian
2026-06-07 23:04:23 +08:00
parent d0d7d8428d
commit 287eab80a9
67 changed files with 1765 additions and 680 deletions

View File

@@ -274,68 +274,90 @@ class _ExercisePlanCreatePageState extends ConsumerState<ExercisePlanCreatePage>
}
}
/// 复查列表
class FollowUpListPage extends ConsumerWidget {
/// 复查列表(从服务器读取)
class FollowUpListPage extends ConsumerStatefulWidget {
const FollowUpListPage({super.key});
@override Widget build(BuildContext context, WidgetRef ref) {
@override ConsumerState<FollowUpListPage> createState() => _FollowUpListPageState();
}
class _FollowUpListPageState extends ConsumerState<FollowUpListPage> {
Future<List<Map<String, dynamic>>>? _future;
@override void initState() { super.initState(); _load(); }
void _load() => setState(() { _future = ref.read(followUpServiceProvider).getList(); });
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('复查随访'), centerTitle: true),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context),
child: const Icon(Icons.add),
backgroundColor: const Color(0xFF8B9CF7),
),
body: ListView(children: _mockFollowUps.map((item) => _FollowUpItem(item: item)).toList()),
);
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('添加复查提醒'),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(decoration: const InputDecoration(labelText: '医院名称')),
const SizedBox(height: 12),
TextField(decoration: const InputDecoration(labelText: '科室')),
const SizedBox(height: 12),
TextField(decoration: const InputDecoration(labelText: '日期', hintText: 'YYYY-MM-DD')),
const SizedBox(height: 12),
TextField(decoration: const InputDecoration(labelText: '备注')),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
TextButton(
onPressed: () {
Navigator.pop(ctx);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('复查提醒已添加 ✅'),
backgroundColor: Color(0xFF8B9CF7),
));
},
child: const Text('保存'),
),
],
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 list = snap.data ?? [];
if (list.isEmpty) {
return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.event_note_outlined, size: 64, color: Colors.grey[300]),
const SizedBox(height: 12),
Text('暂无随访安排', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 4),
const Text('医生创建随访后将显示在这里', style: TextStyle(fontSize: 13, color: Color(0xFF999999))),
]),
);
}
return ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: list.map((item) => _FollowUpItem(item: item)).toList(),
);
},
),
);
}
}
final _mockFollowUps = [
{'id': '1', 'hospital': '协和医院', 'department': '心内科', 'date': '2025-01-20', 'type': '复诊', 'status': 'upcoming', 'notes': '常规复查,带齐病历'},
{'id': '2', 'hospital': '人民医院', 'department': '骨科', 'date': '2025-01-25', 'type': '复查', 'status': 'upcoming', 'notes': '术后3个月复查'},
{'id': '3', 'hospital': '协和医院', 'department': '心内科', 'date': '2024-12-15', 'type': '复诊', 'status': 'completed', 'notes': '已完成'},
];
class _FollowUpItem extends StatelessWidget {
final Map<String, dynamic> item;
const _FollowUpItem({required this.item});
String _statusLabel(String? status) {
switch (status) {
case 'Upcoming': return '即将到来';
case 'Completed': return '已完成';
case 'Cancelled': return '已取消';
default: return status ?? '';
}
}
Color _statusColor(String? status) {
switch (status) {
case 'Upcoming': return const Color(0xFF4F6EF7);
case 'Completed': return const Color(0xFF43A047);
case 'Cancelled': return const Color(0xFFE53935);
default: return const Color(0xFF999999);
}
}
Color _statusBg(String? status) {
switch (status) {
case 'Upcoming': return const Color(0xFFEDF2FF);
case 'Completed': return const Color(0xFFDCFCE7);
case 'Cancelled': return const Color(0xFFFFF5F5);
default: return const Color(0xFFF5F5F5);
}
}
@override
Widget build(BuildContext context) {
final isCompleted = item['status'] == 'completed';
final status = item['status']?.toString();
final label = _statusLabel(status);
final color = _statusColor(status);
final bg = _statusBg(status);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
@@ -346,29 +368,32 @@ class _FollowUpItem extends StatelessWidget {
Row(children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isCompleted ? const Color(0xFFDCFCE7) : const Color(0xFFFEFCE8),
borderRadius: BorderRadius.circular(8),
),
child: Text(
isCompleted ? '已完成' : '待就诊',
style: TextStyle(fontSize: 12, color: isCompleted ? const Color(0xFF43A047) : const Color(0xFFF59E0B)),
),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(8)),
child: Text(label, style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w500)),
),
const SizedBox(width: 8),
Text(item['type']?.toString() ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
const Spacer(),
if (item['doctorName'] != null)
Text('👨‍⚕️ ${item['doctorName']}', style: const TextStyle(fontSize: 12, color: Color(0xFF999999))),
]),
const SizedBox(height: 12),
Text(item['hospital']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
Text(item['title']?.toString() ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('${item['department']} · ${item['date']}', style: TextStyle(fontSize: 14, color: Colors.grey[500])),
if (item['scheduledAt'] != null)
Text(_formatDateTime(item['scheduledAt']?.toString()), style: TextStyle(fontSize: 14, color: Colors.grey[500])),
if ((item['notes']?.toString() ?? '').isNotEmpty) ...[
const SizedBox(height: 8),
Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
Text(item['notes']?.toString() ?? '', style: TextStyle(fontSize: 13, color: Colors.grey[600])),
],
]),
);
}
String _formatDateTime(String? iso) {
if (iso == null) return '';
final dt = DateTime.tryParse(iso);
if (dt == null) return iso;
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
}
/// 健康档案(可编辑)