feat: 饮食记录逻辑重构 + 通知管理分隔线修复 + 多页面 UI 调整

- 饮食: 新增 DietCommentaryPolicy + diet_record_logic, 饮食记录逻辑抽取复用
- 通知管理: 分隔线改为仿通知中心样式, 跳过图标区域只留文字下方
- 侧边栏: 对话记录操作栏浮层修复 + 常用功能间距收紧
- 智能体: 欢迎卡片去 400ms 延迟 + 胶囊去阴影
- 后端: AI 会话仓库新增方法 + 饮食评论策略 + 健康记录规则调整
- 其他: api_client IP 适配 + 多页面 UI 微调 + 新增测试
This commit is contained in:
MingNian
2026-07-12 22:49:38 +08:00
parent d82e006cf4
commit 335a3e6440
33 changed files with 2056 additions and 1283 deletions

View File

@@ -263,10 +263,10 @@ class ChatMessagesView extends ConsumerWidget {
const SizedBox(height: 9),
Text(
info.$2,
style: AppTextStyles.chatTitle.copyWith(
fontSize: 22,
fontWeight: FontWeight.w900,
),
style: AppTextStyles.chatTitle.copyWith(
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 8),
Text(
@@ -473,6 +473,11 @@ class ChatMessagesView extends ConsumerWidget {
backendType != 'exercise');
final isExercise = backendType == 'exercise';
final isHealth = !isMedication && !isExercise;
final visual = isMedication
? AppModuleVisuals.medication
: isExercise
? AppModuleVisuals.exercise
: AppModuleVisuals.health;
// 根据类型获取显示字段
String title = '数据确认';
@@ -585,7 +590,7 @@ class ChatMessagesView extends ConsumerWidget {
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: const Color(0xFF6366F1).withValues(alpha: 0.10),
color: visual.color.withValues(alpha: 0.10),
blurRadius: 28,
offset: const Offset(0, 14),
),
@@ -604,11 +609,11 @@ class ChatMessagesView extends ConsumerWidget {
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 20, 20, 18),
decoration: const BoxDecoration(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFFFFFF), Color(0xFFF7F4FF)],
colors: [Colors.white, visual.lightColor],
),
),
child: Row(
@@ -618,11 +623,11 @@ class ChatMessagesView extends ConsumerWidget {
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: AppColors.actionOutlineGradient,
gradient: visual.gradient,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.auraIndigo.withValues(alpha: 0.18),
color: visual.color.withValues(alpha: 0.18),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -674,7 +679,7 @@ class ChatMessagesView extends ConsumerWidget {
child: Container(
padding: const EdgeInsets.all(1.4),
decoration: BoxDecoration(
gradient: AppColors.actionOutlineGradient,
gradient: visual.gradient,
borderRadius: BorderRadius.circular(20),
),
child: Container(
@@ -691,7 +696,7 @@ class ChatMessagesView extends ConsumerWidget {
width: 68,
height: 68,
decoration: BoxDecoration(
gradient: AppColors.lightGradient,
color: visual.lightColor,
borderRadius: BorderRadius.circular(18),
),
child: Center(
@@ -699,14 +704,14 @@ class ChatMessagesView extends ConsumerWidget {
? Icon(
_getMetricIconData(backendType),
size: 34,
color: AppColors.primaryDark,
color: visual.color,
)
: Icon(
isMedication
? Icons.medication_liquid_outlined
: LucideIcons.footprints,
size: 36,
color: AppColors.iconColor,
color: visual.color,
),
),
),
@@ -745,7 +750,7 @@ class ChatMessagesView extends ConsumerWidget {
fontSize: 19,
color: abnormal
? AppColors.error
: AppColors.iconColor,
: visual.color,
fontWeight: FontWeight.w500,
),
),
@@ -849,11 +854,11 @@ class ChatMessagesView extends ConsumerWidget {
height: 56,
padding: const EdgeInsets.all(1.4),
decoration: BoxDecoration(
gradient: AppColors.actionOutlineGradient,
gradient: visual.gradient,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.auraIndigo.withValues(alpha: 0.18),
color: visual.color.withValues(alpha: 0.18),
blurRadius: 18,
offset: const Offset(0, 8),
),
@@ -913,7 +918,7 @@ class ChatMessagesView extends ConsumerWidget {
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: AppColors.actionOutlineGradient,
gradient: visual.gradient,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
@@ -1268,7 +1273,6 @@ class ChatMessagesView extends ConsumerWidget {
return path;
}
/// 处理 AI 回复里的 markdown 链接点击:
/// - app://diet → 触发拍照/相册选择,跳到饮食拍照流程
/// - app://report → 跳到报告列表(用户可在那里上传新报告)
@@ -1504,27 +1508,58 @@ class ChatMessagesView extends ConsumerWidget {
wtText == null;
// ── 多指标异常检测 ──
// 优先使用后端保存时计算出的 IsAbnormal兜底规则必须和后端 HealthRecordRules 保持一致。
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 偏高');
final bpAbnormal = _recordIsAbnormal(
bp,
fallback: () {
if (bp is! Map) return false;
final s = _numValue(bp['systolic'] ?? bp['Systolic']);
final d = _numValue(bp['diastolic'] ?? bp['Diastolic']);
return s != null &&
d != null &&
(s >= 140 || d >= 90 || s <= 89 || d <= 59);
},
);
if (bpAbnormal && bp is Map) {
final s = _numValue(bp['systolic'] ?? bp['Systolic']);
final d = _numValue(bp['diastolic'] ?? bp['Diastolic']);
abnormals.add(
'血压 ${s?.toStringAsFixed(0) ?? '--'}/${d?.toStringAsFixed(0) ?? '--'} 异常',
);
}
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)} 偏慢');
}
final hrAbnormal = _recordIsAbnormal(
hr,
fallback: () {
final v = _numValue(hr is Map ? hr['value'] ?? hr['Value'] : null);
return v != null && (v > 100 || v < 60);
},
);
if (hrAbnormal && hr is Map) {
final v = _numValue(hr['value'] ?? hr['Value']);
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)} 偏高');
final bsAbnormal = _recordIsAbnormal(
bs,
fallback: () {
final v = _numValue(bs is Map ? bs['value'] ?? bs['Value'] : null);
return v != null && (v > 6.1 || v < 3.9);
},
);
if (bsAbnormal && bs is Map) {
final v = _numValue(bs['value'] ?? bs['Value']);
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 boAbnormal = _recordIsAbnormal(
bo,
fallback: () {
final v = _numValue(bo is Map ? bo['value'] ?? bo['Value'] : null);
return v != null && v <= 94;
},
);
if (boAbnormal && bo is Map) {
final v = _numValue(bo['value'] ?? bo['Value']);
abnormals.add('血氧 ${v?.toStringAsFixed(0) ?? '--'}% 异常');
}
final hasAbnormal = abnormals.isNotEmpty;
@@ -1545,8 +1580,8 @@ class ChatMessagesView extends ConsumerWidget {
'健康指标',
trailing: trailing,
status: 'warning',
iconColor: healthIconColor,
iconBg: healthIconBg,
iconColor: AppColors.warning,
iconBg: AppColors.warningLight,
onTap: () => pushRoute(ref, 'trend'),
),
);
@@ -1749,7 +1784,7 @@ class ChatMessagesView extends ConsumerWidget {
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
padding: const EdgeInsets.fromLTRB(16, 13, 16, 11),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
@@ -1760,26 +1795,13 @@ class ChatMessagesView extends ConsumerWidget {
),
child: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.health_and_safety,
size: 16,
color: AppColors.primary,
),
),
const SizedBox(width: 8),
const Text(
'今日健康',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
fontSize: 16,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
height: 1.15,
),
),
const Spacer(),
@@ -1867,6 +1889,8 @@ class ChatMessagesView extends ConsumerWidget {
Expanded(
child: Text(
trailing ?? label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
color: AppColors.textPrimary,
@@ -1884,6 +1908,27 @@ class ChatMessagesView extends ConsumerWidget {
),
);
}
bool _recordIsAbnormal(dynamic record, {required bool Function() fallback}) {
if (record is Map) {
final raw =
record['isAbnormal'] ?? record['IsAbnormal'] ?? record['abnormal'];
if (raw is bool) return raw;
if (raw is String) {
final normalized = raw.toLowerCase();
if (normalized == 'true') return true;
if (normalized == 'false') return false;
}
if (raw is num) return raw != 0;
}
return fallback();
}
double? _numValue(dynamic value) {
if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value);
return null;
}
}
class _AgentMark extends StatelessWidget {
@@ -1906,7 +1951,7 @@ class _AgentMark extends StatelessWidget {
? Alignment.bottomCenter
: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(colors.verticalGradient ? 18 : 20),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: colors.accent.withValues(alpha: 0.18),