fix: 全面修复 - 实时通讯、报告AI流程、健康概览、用药运动提醒
- SignalR Hub消息持久化+防重复+跨端广播 - 报告VLM+LLM真实AI流程(验证→提取→解读) - 医生审核页重构(严重程度/建议模板/综合评语) - 健康概览五合一趋势图(fl_chart+指标切换) - 用药提醒时区修复+跨午夜+过期提醒 - 运动打卡DayOfWeek映射修复+计划覆盖查询 - 体重与其他四指标同级(Abnormal检查/确认卡牌) - AI Agent支持多种类多时段批量录入 - 删除硬编码假数据(张三/假医生/假AI解读) - 随访/报告审核数据链路全部打通
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user