feat: 饮食记录逻辑重构 + 通知管理分隔线修复 + 多页面 UI 调整
- 饮食: 新增 DietCommentaryPolicy + diet_record_logic, 饮食记录逻辑抽取复用 - 通知管理: 分隔线改为仿通知中心样式, 跳过图标区域只留文字下方 - 侧边栏: 对话记录操作栏浮层修复 + 常用功能间距收紧 - 智能体: 欢迎卡片去 400ms 延迟 + 胶囊去阴影 - 后端: AI 会话仓库新增方法 + 饮食评论策略 + 健康记录规则调整 - 其他: api_client IP 适配 + 多页面 UI 微调 + 新增测试
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user