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

@@ -153,17 +153,22 @@ class ChatMessagesView extends ConsumerWidget {
),
),
// ── 快捷操作按钮网格 ──
Padding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 4),
child: Wrap(
spacing: 10,
runSpacing: 10,
children: agent == ActiveAgent.consultation
? _buildDoctorCards(screenWidth, ref)
: actions.map((a) => _agentActionBtn(a, screenWidth, context, ref, colors)).toList(),
// ── 医生列表(一行三列)──
if (agent == ActiveAgent.consultation) ...[
Padding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 4),
child: _buildDoctorCards(ref),
),
),
] else ...[
Padding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 4),
child: Wrap(
spacing: 10,
runSpacing: 10,
children: actions.map((a) => _agentActionBtn(a, screenWidth, context, ref, colors)).toList(),
),
),
],
const SizedBox(height: 12),
],
@@ -214,35 +219,41 @@ class ChatMessagesView extends ConsumerWidget {
);
}
List<Widget> _buildDoctorCards(double screenWidth, WidgetRef ref) {
Widget _buildDoctorCards(WidgetRef ref) {
const doctors = [
{'name': '医生', 'title': '主任医师', 'dept': '', 'desc': '冠心病、高血压术后管理', 'id': 'doc_1'},
{'name': '医生', 'title': '副主任医师', 'dept': '内分泌', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'doc_2'},
{'name': '医生', 'title': '医师', 'dept': '营养', 'desc': '术后营养指导、饮食方案制定', 'id': 'doc_3'},
{'name': '', 'title': '主任医师', 'dept': '脏康复', 'desc': '冠心病、高血压术后管理', 'id': '468b82e2-d95a-4436-bff6-a50eecf99a66'},
{'name': '', 'title': '副主任医师', 'dept': '营养', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'd4148733-b538-4398-af17-0c7592fc0c2d'},
{'name': '建国', 'title': '医师', 'dept': '心血管内', 'desc': '术后营养指导、饮食方案制定', 'id': 'ef0953c9-eb63-4d03-b6d7-050a1897d4a3'},
];
return doctors.map((d) => _doctorCard(d, screenWidth, ref)).toList();
return Row(
children: [
for (var i = 0; i < doctors.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(child: _doctorCard(doctors[i], ref)),
],
],
);
}
Widget _doctorCard(Map<String, String> doc, double screenWidth, WidgetRef ref) {
Widget _doctorCard(Map<String, String> doc, WidgetRef ref) {
return InkWell(
onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}),
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(12),
child: Container(
width: screenWidth * 0.38,
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFF0F2FF)),
),
child: Column(children: [
child: Column(mainAxisSize: MainAxisSize.min, children: [
CircleAvatar(
radius: 24,
radius: 22,
backgroundColor: const Color(0xFFF0F2FF),
child: Text(doc['name']![0], style: const TextStyle(fontSize: 20, color: Color(0xFF8B9CF7))),
child: Text(doc['name']![0], style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF8B9CF7))),
),
const SizedBox(height: 8),
Text(doc['name']!, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
Text(doc['name']!, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF333333))),
Text(doc['title']!, style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
const SizedBox(height: 2),
Container(
@@ -253,12 +264,13 @@ class ChatMessagesView extends ConsumerWidget {
),
child: Text(doc['dept']!, style: const TextStyle(fontSize: 10, color: Color(0xFF8B9CF7))),
),
const SizedBox(height: 6),
const SizedBox(height: 4),
Text(
doc['desc']!,
style: const TextStyle(fontSize: 10, color: Color(0xFF888888), height: 1.3),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
]),
),
@@ -1219,10 +1231,23 @@ class ChatMessagesView extends ConsumerWidget {
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;
// 判断该用药时间是否已过
bool overdue = false;
if (m['timeOfDay'] is List) {
final nowTime = TimeOfDay.fromDateTime(now);
for (final t in (m['timeOfDay'] as List)) {
final parts = t.toString().split(':');
final h = int.tryParse(parts[0]) ?? 0;
final min = int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0;
if (nowTime.hour > h || (nowTime.hour == h && nowTime.minute > min)) {
overdue = true;
break;
}
}
}
tasks.add(_taskRow(
context, Icons.medication_rounded, '$name $dosage ($times)',
status: medOverdue ? 'overdue' : 'pending',
status: overdue ? 'overdue' : 'pending',
onTap: () {},
));
}
@@ -1231,15 +1256,15 @@ class ChatMessagesView extends ConsumerWidget {
tasks.add(_taskRow(context, Icons.medication_rounded, '暂无用药提醒', status: 'pending'));
}
// 3. 运动 — 从所有计划中找今日待打卡项
final plans = ref.read(exerciseServiceProvider).getPlans();
plans.then((list) {
for (final p in list) {
final items = (p['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
final today = now.weekday - 1;
// 3. 运动 — 使用 currentExercisePlanProvider 同步读取
final exercisePlan = ref.watch(currentExercisePlanProvider);
exercisePlan.whenOrNull(data: (plan) {
if (plan != null) {
final items = (plan['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
final today = now.weekday % 7; // Dart: Mon=1...Sun=7 → C#: Sun=0 Mon=1...Sat=6
final todayItem = items.cast<Map<String, dynamic>?>().firstWhere(
(i) => i?['dayOfWeek'] == today, orElse: () => null);
if (todayItem != null) {
if (todayItem != null && todayItem['isRestDay'] != true) {
final done = todayItem['isCompleted'] == true;
final name = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动';
final dur = items.isNotEmpty ? (items.first['durationMinutes']?.toString() ?? '30') : '30';
@@ -1248,19 +1273,11 @@ class ChatMessagesView extends ConsumerWidget {
status: done ? 'done' : (now.hour >= 18 ? 'overdue' : 'pending'),
onTap: () => pushRoute(ref, 'exercisePlan'),
));
break;
}
}
});
// 4. 测量
tasks.add(_taskRow(
context, Icons.today, '今日测量:血压',
status: 'pending',
onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}),
));
// 5. 异常指标
// 4. 异常指标
if (bp is Map) {
final s = bp['systolic'];
if (s is int && s >= 140) {