feat: 健康录入提醒 + 多指标异常 + 用药漏服 + UI 优化
后端 - NotificationPreference 增加健康录入提醒总开关和 5 子项(血压/心率/血糖/血氧/体重) - EF 迁移 AddHealthRecordReminder - 新增 GET/PUT /api/notification-prefs - 新增 HealthRecordReminderService 每天 9 点/12 点扫描未录入用户推送提醒 前端 - 通知偏好对接真实后端 API - 设置页新增「健康录入提醒」总开关+5 子项 - 通知中心右上角加齿轮跳设置页 - 今日健康卡片:4 项异常检测+多项合并;用药只显漏服并按时间窗口区分文案 - 侧边栏头像改圆形白底灰图标,去掉紫色品牌渐变 - 整体背景统一浅灰白 #F6F8FC,去掉浅紫渐变 - 设置页简化(去掉分组/彩色图标方块) - 健康档案保存按钮移到右上角,TextButton 全局默认黑色 - API baseUrl 跟随网络环境
This commit is contained in:
@@ -1484,39 +1484,44 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
boText == null &&
|
||||
wtText == null;
|
||||
|
||||
// 异常检测
|
||||
final bpAbnormal =
|
||||
bp is Map && bp['systolic'] is int && (bp['systolic'] as int) >= 140;
|
||||
final hasAbnormal = bpAbnormal;
|
||||
// ── 多指标异常检测 ──
|
||||
final abnormals = <String>[];
|
||||
if (bp is Map && bp['systolic'] is int) {
|
||||
final s = bp['systolic'] as int;
|
||||
final d = bp['diastolic'] is int ? bp['diastolic'] as int : 0;
|
||||
if (s >= 140 || d >= 90) abnormals.add('血压 $s/$d 偏高');
|
||||
}
|
||||
if (hr is Map && hr['value'] is num) {
|
||||
final v = (hr['value'] as num).toDouble();
|
||||
if (v > 100) abnormals.add('心率 ${v.toStringAsFixed(0)} 偏快');
|
||||
else if (v < 60) abnormals.add('心率 ${v.toStringAsFixed(0)} 偏慢');
|
||||
}
|
||||
if (bs is Map && bs['value'] is num) {
|
||||
final v = (bs['value'] as num).toDouble();
|
||||
if (v > 6.1) abnormals.add('血糖 ${v.toStringAsFixed(1)} 偏高');
|
||||
}
|
||||
if (bo is Map && bo['value'] is num) {
|
||||
final v = (bo['value'] as num).toDouble();
|
||||
if (v < 95) abnormals.add('血氧 ${v.toStringAsFixed(0)}% 偏低');
|
||||
}
|
||||
final hasAbnormal = abnormals.isNotEmpty;
|
||||
|
||||
// ── 1. 健康指标 ──
|
||||
// 没记录则整行不显示(避免每天唠叨提醒)
|
||||
const healthIconColor = Color(0xFF3B82F6);
|
||||
const healthIconBg = Color(0xFFDBEAFE);
|
||||
if (allNull) {
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.monitor_heart_outlined,
|
||||
'健康指标',
|
||||
trailing: '今日暂无记录,点击录入',
|
||||
status: 'pending',
|
||||
iconColor: healthIconColor,
|
||||
iconBg: healthIconBg,
|
||||
onTap: () => pushRoute(ref, 'trend'),
|
||||
),
|
||||
);
|
||||
// 不显示
|
||||
} else if (hasAbnormal) {
|
||||
// 有异常时显示异常指标
|
||||
final abnormalParts = <String>[];
|
||||
if (bpAbnormal) {
|
||||
abnormalParts.add('血压 ${bp['systolic']}/${bp['diastolic']} 偏高');
|
||||
}
|
||||
final trailing = abnormals.length == 1
|
||||
? abnormals.first
|
||||
: '${abnormals.length} 项指标异常';
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.warning_amber_rounded,
|
||||
'健康指标',
|
||||
trailing: abnormalParts.join(' · '),
|
||||
trailing: trailing,
|
||||
status: 'warning',
|
||||
iconColor: healthIconColor,
|
||||
iconBg: healthIconBg,
|
||||
@@ -1626,72 +1631,74 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
}
|
||||
|
||||
// ── 3. 用药打卡 ──
|
||||
// 只显示已过期(漏服)的药;用颜色区分严重程度;没漏服则整行不显示
|
||||
const medIconColor = Color(0xFFEC4899);
|
||||
const medIconBg = Color(0xFFFCE7F3);
|
||||
reminders.whenOrNull(
|
||||
data: (meds) {
|
||||
if (meds.isNotEmpty) {
|
||||
final overdueMeds = meds
|
||||
.where((m) => m['status'] == 'overdue')
|
||||
.toList();
|
||||
final upcomingMeds = meds
|
||||
.where((m) => m['status'] == 'upcoming')
|
||||
.toList();
|
||||
final takenMeds = meds.where((m) => m['status'] == 'taken').toList();
|
||||
// 未服/过期的在前面
|
||||
for (final m in [...overdueMeds, ...upcomingMeds]) {
|
||||
final name = m['name']?.toString() ?? '药品';
|
||||
final time = m['scheduledTime']?.toString() ?? '';
|
||||
final isOverdue = m['status'] == 'overdue';
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.medication_rounded,
|
||||
name,
|
||||
trailing: '${isOverdue ? "❗过期" : "⏳待服"} $time',
|
||||
status: isOverdue ? 'overdue' : 'pending',
|
||||
iconColor: medIconColor,
|
||||
iconBg: medIconBg,
|
||||
onTap: () => pushRoute(ref, 'medCheckIn'),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 已服的合并显示
|
||||
if (takenMeds.isNotEmpty) {
|
||||
final names = takenMeds
|
||||
.map((m) => m['name']?.toString() ?? '药品')
|
||||
.join('、');
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.check_circle,
|
||||
names,
|
||||
trailing: '已服用 (${takenMeds.length}项)',
|
||||
status: 'done',
|
||||
iconColor: medIconColor,
|
||||
iconBg: medIconBg,
|
||||
),
|
||||
);
|
||||
final overdueMeds = meds
|
||||
.where((m) => m['status'] == 'overdue')
|
||||
.toList();
|
||||
if (overdueMeds.isEmpty) return;
|
||||
|
||||
// 按过期时长排序——先把最严重的放前面
|
||||
final now = DateTime.now();
|
||||
final withDelta = overdueMeds.map((m) {
|
||||
final timeStr = m['scheduledTime']?.toString() ?? '';
|
||||
final parts = timeStr.split(':');
|
||||
DateTime? scheduled;
|
||||
if (parts.length >= 2) {
|
||||
final h = int.tryParse(parts[0]);
|
||||
final mn = int.tryParse(parts[1]);
|
||||
if (h != null && mn != null) {
|
||||
scheduled = DateTime(now.year, now.month, now.day, h, mn);
|
||||
}
|
||||
}
|
||||
final overdueHours = scheduled == null
|
||||
? 0
|
||||
: now.difference(scheduled).inMinutes / 60.0;
|
||||
return (m, overdueHours);
|
||||
}).toList()
|
||||
..sort((a, b) => b.$2.compareTo(a.$2));
|
||||
|
||||
// 取最严重那一项作为代表,其他用"等"省略
|
||||
final first = withDelta.first;
|
||||
final firstName = first.$1['name']?.toString() ?? '药品';
|
||||
final firstTime = first.$1['scheduledTime']?.toString() ?? '';
|
||||
final firstHours = first.$2;
|
||||
|
||||
// 过期 < 1h 提醒服药;1-4h 可补服(红色);>4h 已错过窗口(灰色提示但不强警告)
|
||||
final String trailing;
|
||||
final String status;
|
||||
if (firstHours < 1) {
|
||||
trailing = '$firstTime 该服药了';
|
||||
status = 'pending';
|
||||
} else if (firstHours < 4) {
|
||||
trailing = '$firstTime 已漏服 ${firstHours.toStringAsFixed(0)} 小时,可补服';
|
||||
status = 'overdue';
|
||||
} else {
|
||||
trailing = '$firstTime 已错过,请等下次按时服用';
|
||||
status = 'overdue';
|
||||
}
|
||||
|
||||
final title = overdueMeds.length == 1
|
||||
? firstName
|
||||
: '$firstName 等 ${overdueMeds.length} 种';
|
||||
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.medication_rounded,
|
||||
title,
|
||||
trailing: trailing,
|
||||
status: status,
|
||||
iconColor: medIconColor,
|
||||
iconBg: medIconBg,
|
||||
onTap: () => pushRoute(ref, 'medCheckIn'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
final hasMeds =
|
||||
reminders.asData?.value != null && (reminders.asData!.value).isNotEmpty;
|
||||
if (!hasMeds) {
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.medication_rounded,
|
||||
'用药打卡',
|
||||
trailing: '暂无用药提醒',
|
||||
status: 'pending',
|
||||
iconColor: medIconColor,
|
||||
iconBg: medIconBg,
|
||||
onTap: () => pushRoute(ref, 'medications'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
|
||||
Reference in New Issue
Block a user