- AgentWelcomeCard:紫色渐变头部+快捷按钮网格+智能体描述 - DataConfirmCard:绿色渐变确认条+迷你趋势图+编辑/确认按钮 - MedicationConfirmCard:药丸图标+剩余药量进度条+确认/跳过 - DietAnalysisCard:大号热量+营养素圆环+食物明细+AI建议 - ReportAnalysisCard:指标表格+异常高亮+AI解读 - trend_page 重写:CustomPaint 平滑曲线+当前值卡片+统计摘要 - chat_provider 新增 agentWelcome 消息类型
1228 lines
55 KiB
Dart
1228 lines
55 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../../../providers/chat_provider.dart';
|
||
|
||
/// 对话消息列表
|
||
class ChatMessagesView extends ConsumerWidget {
|
||
final ScrollController scrollCtrl;
|
||
final List<ChatMessage> messages;
|
||
|
||
const ChatMessagesView({super.key, required this.scrollCtrl, required this.messages});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final chatState = ref.watch(chatProvider);
|
||
|
||
if (messages.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 80,
|
||
height: 80,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFEDEBFF),
|
||
borderRadius: BorderRadius.circular(40),
|
||
),
|
||
child: const Icon(Icons.health_and_safety, size: 40, color: Color(0xFF635BFF)),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium),
|
||
const SizedBox(height: 8),
|
||
const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 14, color: Color(0xFF9E9E9E))),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return ListView.builder(
|
||
controller: scrollCtrl,
|
||
reverse: true,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
itemCount: messages.length,
|
||
itemBuilder: (context, index) {
|
||
final msg = messages[messages.length - 1 - index];
|
||
return _buildMessageContent(context, msg, chatState);
|
||
},
|
||
);
|
||
}
|
||
|
||
// ─── 消息分发 ─────────────────────────────────────────────
|
||
|
||
Widget _buildMessageContent(BuildContext context, ChatMessage msg, ChatState chatState) {
|
||
final isUser = msg.isUser;
|
||
|
||
if (!isUser && chatState.isStreaming && msg.content.isEmpty) {
|
||
return _buildThinkingBubble(context, chatState.thinkingText);
|
||
}
|
||
|
||
switch (msg.type) {
|
||
case MessageType.agentWelcome:
|
||
return _buildAgentWelcomeCard(context, msg, chatState.activeAgent);
|
||
case MessageType.dataConfirm:
|
||
return _buildDataConfirmCard(context, msg);
|
||
case MessageType.medicationConfirm:
|
||
return _buildMedicationConfirmCard(context, msg);
|
||
case MessageType.dietAnalysis:
|
||
return _buildDietAnalysisCard(context, msg);
|
||
case MessageType.reportAnalysis:
|
||
return _buildReportAnalysisCard(context, msg);
|
||
case MessageType.quickOptions:
|
||
return _buildQuickOptionsCard(context, msg);
|
||
default:
|
||
return _buildTextBubble(context, msg);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 1. AgentWelcomeCard — 智能体欢迎卡片
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildAgentWelcomeCard(BuildContext context, ChatMessage msg, ActiveAgent agent) {
|
||
final info = _agentInfo(agent);
|
||
final actions = agent.actions;
|
||
final screenWidth = MediaQuery.of(context).size.width;
|
||
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: screenWidth * 0.92),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFFFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [
|
||
BoxShadow(color: const Color(0xFF635BFF).withAlpha(20), blurRadius: 16, offset: const Offset(0, 4)),
|
||
],
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// ── 紫色渐变头部 ──
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.fromLTRB(20, 24, 16, 20),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [Color(0xFF7C73FF), Color(0xFF635BFF), Color(0xFF5241D9)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
width: 48,
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withAlpha(30),
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
child: Icon(info.$1, size: 26, color: Colors.white),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(info.$2, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)),
|
||
const SizedBox(height: 4),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withAlpha(25),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Text(info.$3, style: const TextStyle(fontSize: 12, color: Color(0xFFE0DDFF))),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () {},
|
||
child: Container(
|
||
width: 28,
|
||
height: 28,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withAlpha(20),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: const Icon(Icons.close, size: 16, color: Color(0xFFE0DDFF)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// ── 快捷操作按钮网格 ──
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 4),
|
||
child: Wrap(
|
||
spacing: 10,
|
||
runSpacing: 10,
|
||
children: actions.map((a) => _agentActionBtn(a, screenWidth)).toList(),
|
||
),
|
||
),
|
||
|
||
// ── 底部提示 ──
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(0, 12, 0, 18),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(width: 24, height: 1, color: const Color(0xFFD0CCED)),
|
||
const Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||
child: Text('或直接对我说...', style: TextStyle(fontSize: 13, color: Color(0xFF9E94CF))),
|
||
),
|
||
Container(width: 24, height: 1, color: const Color(0xFFD0CCED)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _agentActionBtn(_AgentAction a, double screenWidth) {
|
||
return InkWell(
|
||
onTap: () {},
|
||
borderRadius: BorderRadius.circular(14),
|
||
child: Container(
|
||
width: ((screenWidth - 72) / (a.isWide ? 2 : 3)) - 10,
|
||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF7F5FF),
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(color: const Color(0xFFEBE8FF), width: 1),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 38,
|
||
height: 38,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFEDEAFF),
|
||
borderRadius: BorderRadius.circular(11),
|
||
),
|
||
child: Icon(a.icon, size: 20, color: const Color(0xFF635BFF)),
|
||
),
|
||
const SizedBox(height: 7),
|
||
Text(a.label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF333333))),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 2. DataConfirmCard — 增强版数据确认卡片
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildDataConfirmCard(BuildContext context, ChatMessage msg) {
|
||
final meta = msg.metadata;
|
||
final metricType = meta?['type'] as String? ?? '';
|
||
final value = meta?['value'] as String? ?? '';
|
||
final abnormal = meta?['abnormal'] as bool? ?? false;
|
||
final recordTime = meta?['recordTime'] as String? ?? '';
|
||
final unit = meta?['unit'] as String? ?? _getMetricUnit(metricType);
|
||
final trend = meta?['trend'] as List? ?? [0.6, 0.8, 0.5];
|
||
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFFFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// ── 绿色勾选条 ──
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(colors: [Color(0xFF4CAF50), Color(0xFF43A047)]),
|
||
),
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.check_circle, size: 18, color: Colors.white),
|
||
SizedBox(width: 6),
|
||
Text('✓ 数据已记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white)),
|
||
],
|
||
),
|
||
),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.all(18),
|
||
child: Column(
|
||
children: [
|
||
// 记录时间
|
||
Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(recordTime.isNotEmpty ? recordTime : _formatTime(msg.createdAt), style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// 主要指标区域
|
||
Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF9F8FF),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 52,
|
||
height: 52,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFEDEAFF),
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
child: Center(child: Text(_getMetricIcon(metricType), style: const TextStyle(fontSize: 26))),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(_getMetricName(metricType), style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
|
||
const SizedBox(height: 4),
|
||
RichText(
|
||
text: TextSpan(
|
||
children: [
|
||
TextSpan(text: value, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: abnormal ? const Color(0xFFE53935) : const Color(0xFF1A1A2E))),
|
||
TextSpan(text: ' $unit', style: TextStyle(fontSize: 14, color: abnormal ? const Color(0xFFE53970) : const Color(0xFF999999))),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 异常警告条
|
||
if (abnormal) ...[
|
||
const SizedBox(height: 12),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFF3F0),
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(color: const Color(0xFFFFDAD4), width: 1),
|
||
),
|
||
child: const Row(
|
||
children: [
|
||
Icon(Icons.warning_amber_rounded, size: 18, color: Color(0xFFE53935)),
|
||
SizedBox(width: 8),
|
||
Expanded(child: Text('⚠️ 数值偏高,建议关注', style: TextStyle(fontSize: 13, color: Color(0xFFE53935), fontWeight: FontWeight.w500))),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
|
||
// 迷你趋势图(最近3次)
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
children: [
|
||
const Text('近期趋势', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||
const Spacer(),
|
||
const Text('最近3次', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
SizedBox(
|
||
height: 36,
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: trend.asMap().entries.map((e) {
|
||
final h = (e.value * 32).clamp(6.0, 32.0);
|
||
return Padding(
|
||
padding: EdgeInsets.only(right: e.key < trend.length - 1 ? 10 : 0),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 22,
|
||
height: h,
|
||
decoration: BoxDecoration(
|
||
color: e.key == trend.length - 1 ? const Color(0xFF635BFF) : const Color(0xFFD5D0FF),
|
||
borderRadius: BorderRadius.circular(5),
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text('${e.key + 1}', style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
|
||
// 底部操作按钮
|
||
const SizedBox(height: 18),
|
||
Row(children: [
|
||
Expanded(child: _cardOutlineBtn('编辑', Icons.edit_outlined)),
|
||
const SizedBox(width: 8),
|
||
Expanded(child: _cardFilledBtn('确认', Icons.check)),
|
||
const SizedBox(width: 8),
|
||
Expanded(child: _cardOutlineBtn('查看详情', Icons.trending_up_outlined)),
|
||
]),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 3. MedicationConfirmCard — 增强版用药确认卡片
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildMedicationConfirmCard(BuildContext context, ChatMessage msg) {
|
||
final meta = msg.metadata;
|
||
final name = meta?['name'] as String? ?? '';
|
||
final dosage = meta?['dosage'] as String? ?? '';
|
||
final time = meta?['time'] as String? ?? '';
|
||
final frequency = meta?['frequency'] as String? ?? '';
|
||
final remaining = meta?['remaining'] as double? ?? 0.65;
|
||
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFFFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// ── 药品头部 ──
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.fromLTRB(18, 20, 18, 16),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(colors: [Color(0xFFE8F0FE), Color(0xFFF5F3FF)]),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 46,
|
||
height: 46,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFF3E0),
|
||
borderRadius: BorderRadius.circular(13),
|
||
),
|
||
child: const Center(child: Text('💊', style: TextStyle(fontSize: 24))),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
|
||
if (dosage.isNotEmpty) ...[
|
||
const SizedBox(height: 3),
|
||
Text(dosage, style: const TextStyle(fontSize: 13, color: Color(0xFF777777))),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 6),
|
||
child: Column(
|
||
children: [
|
||
// 服药时间 & 频率
|
||
_medInfoRow(Icons.schedule_outlined, time.isNotEmpty ? '服药时间:$time' : '待设置'),
|
||
if (frequency.isNotEmpty) _medInfoRow(Icons.repeat, '频率:$frequency'),
|
||
|
||
// 剩余药量进度条
|
||
const SizedBox(height: 14),
|
||
Row(
|
||
children: [
|
||
const Text('剩余药量', style: TextStyle(fontSize: 13, color: Color(0xFF666666))),
|
||
const Spacer(),
|
||
Text('${(remaining * 100).toInt()}%', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF635BFF))),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(6),
|
||
child: LinearProgressIndicator(
|
||
value: remaining,
|
||
minHeight: 8,
|
||
backgroundColor: const Color(0xFFEFEDFF),
|
||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF635BFF)),
|
||
),
|
||
),
|
||
|
||
// 操作按钮
|
||
const SizedBox(height: 18),
|
||
Row(children: [
|
||
Expanded(child: _cardFilledBtn('确认服药', Icons.check_circle_outline)),
|
||
const SizedBox(width: 8),
|
||
Expanded(child: _cardOutlineBtn('跳过', Icons.skip_next)),
|
||
const SizedBox(width: 8),
|
||
Expanded(child: _cardOutlineBtn('设置提醒', Icons.notifications_none_outlined)),
|
||
]),
|
||
const SizedBox(height: 8),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _medInfoRow(IconData icon, String text) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, size: 17, color: const Color(0xFF888888)),
|
||
const SizedBox(width: 8),
|
||
Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF444444))),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 4. DietAnalysisCard — 增强版饮食分析卡片
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildDietAnalysisCard(BuildContext context, ChatMessage msg) {
|
||
final meta = msg.metadata;
|
||
final foods = meta?['foods'] as List? ?? [];
|
||
final totalCalories = meta?['totalCalories'] as int? ?? 0;
|
||
final rating = meta?['rating'] as int? ?? 0;
|
||
final warnings = meta?['warnings'] as List? ?? [];
|
||
final carbs = meta?['carbs'] as double? ?? 50.0;
|
||
final protein = meta?['protein'] as double? ?? 20.0;
|
||
final fat = meta?['fat'] as double? ?? 30.0;
|
||
final advice = meta?['advice'] as String? ?? '饮食均衡,多吃蔬菜水果,减少高油高糖食物摄入。';
|
||
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFFFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// ── 头部 ──
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(colors: [Color(0xFFFFF8E1), Color(0xFFFFF3E0)]),
|
||
),
|
||
child: const Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text('🍽️ ', style: TextStyle(fontSize: 18)),
|
||
Text('饮食分析结果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
|
||
],
|
||
),
|
||
),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 总热量大号数字
|
||
Center(
|
||
child: Column(
|
||
children: [
|
||
Text('$totalCalories', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.w800, color: Color(0xFFFF8F00))),
|
||
const Text('千卡 (kcal)', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 三大营养素圆环指示
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
children: [
|
||
_nutrientRing('碳水', carbs, const Color(0xFF42A5F5), const Color(0xFFBBDEFB)),
|
||
_nutrientRing('蛋白质', protein, const Color(0xFF66BB6A), const Color(0xFFC8E6C9)),
|
||
_nutrientRing('脂肪', fat, const Color(0xFFFFA726), const Color(0xFFFFE0B2)),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 食物列表
|
||
const Text('食物明细', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF333333))),
|
||
const SizedBox(height: 10),
|
||
...foods.map((food) {
|
||
final f = food as Map? ?? {};
|
||
final fCal = (f['calories'] ?? 0) as num;
|
||
final fPct = totalCalories > 0 ? (fCal / totalCalories * 100).clamp(0.0, 100.0) : 0.0;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(f['name'] as String? ?? '', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
||
const Spacer(),
|
||
Text('${fCal.toInt()} kcal', style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(3),
|
||
child: LinearProgressIndicator(
|
||
value: fPct / 100,
|
||
minHeight: 5,
|
||
backgroundColor: const Color(0xFFF0EEFF),
|
||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFFFFB74D)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
|
||
// 健康评分
|
||
const SizedBox(height: 14),
|
||
Row(
|
||
children: [
|
||
const Text('健康评分', style: TextStyle(fontSize: 13, color: Color(0xFF666666))),
|
||
const SizedBox(width: 8),
|
||
...List.generate(5, (i) => Padding(
|
||
padding: const EdgeInsets.only(right: 2),
|
||
child: Icon(i < rating ? Icons.star : Icons.star_border, size: 18, color: i < rating ? const Color(0xFFFFB800) : const Color(0xFFE0E0E0)),
|
||
)),
|
||
const Spacer(),
|
||
Text('$rating/5', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFFFB800))),
|
||
],
|
||
),
|
||
|
||
// 警告
|
||
if (warnings.isNotEmpty) ...[
|
||
const SizedBox(height: 12),
|
||
...warnings.map((w) => Container(
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFBF0),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFFFFE082), width: 0.8),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Text('⚠️ ', style: TextStyle(fontSize: 13)),
|
||
Expanded(child: Text(w.toString(), style: const TextStyle(fontSize: 12, color: Color(0xFFE65100)))),
|
||
],
|
||
),
|
||
)),
|
||
],
|
||
|
||
// AI 建议(可展开)
|
||
const SizedBox(height: 14),
|
||
_ExpandableAdvice(advice: advice),
|
||
const SizedBox(height: 6),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _nutrientRing(String label, double pct, Color fgColor, Color bgColor) {
|
||
return Column(
|
||
children: [
|
||
SizedBox(
|
||
width: 56,
|
||
height: 56,
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
Container(
|
||
width: 56,
|
||
height: 56,
|
||
decoration: BoxDecoration(shape: BoxShape.circle, color: bgColor),
|
||
),
|
||
SizedBox(
|
||
width: 56,
|
||
height: 56,
|
||
child: CircularProgressIndicator(
|
||
value: pct.clamp(0.0, 100.0) / 100,
|
||
strokeWidth: 5,
|
||
backgroundColor: Colors.transparent,
|
||
valueColor: AlwaysStoppedAnimation<Color>(fgColor),
|
||
),
|
||
),
|
||
Text('${pct.toInt()}%', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: fgColor)),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 5),
|
||
Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF888888))),
|
||
],
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 5. ReportAnalysisCard — 增强版报告分析卡片
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildReportAnalysisCard(BuildContext context, ChatMessage msg) {
|
||
final meta = msg.metadata;
|
||
final reportType = meta?['type'] as String? ?? '体检报告';
|
||
final reportDate = meta?['date'] as String? ?? '';
|
||
final indicators = meta?['indicators'] as List? ?? [];
|
||
final summary = meta?['summary'] as String? ?? '';
|
||
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFFFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
|
||
),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// ── 报告头部 ──
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 14),
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(colors: [Color(0xFFE8EAF6), Color(0xFFEDE7F6)]),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFC5CAE9),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: const Icon(Icons.description_outlined, size: 20, color: Color(0xFF3F51B5)),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(reportType, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
|
||
if (reportDate.isNotEmpty)
|
||
Text(reportDate, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 指标表格
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFAFAFC),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFEEEEEE), width: 1),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 表头
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: const BoxDecoration(
|
||
color: Color(0xFFF5F4FA),
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||
),
|
||
child: const Row(
|
||
children: [
|
||
Expanded(flex: 2, child: Text('指标名称', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)))),
|
||
Expanded(flex: 1, child: Text('数值', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)),
|
||
Expanded(flex: 1, child: Text('状态', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)),
|
||
],
|
||
),
|
||
),
|
||
// 数据行
|
||
...indicators.map((ind) {
|
||
final i = ind as Map? ?? {};
|
||
final name = i['name'] as String? ?? '';
|
||
final value = i['value'] as String? ?? '';
|
||
final status = i['status'] as String? ?? 'normal';
|
||
final refRange = i['refRange'] as String? ?? '';
|
||
final isAbnormal = status != 'normal';
|
||
Color sc;
|
||
switch (status) {
|
||
case 'high': sc = const Color(0xFFE53935); break;
|
||
case 'low': sc = const Color(0xFFF9A825); break;
|
||
default: sc = const Color(0xFF43A047);
|
||
}
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: isAbnormal ? const Color(0xFFFFF8F5) : Colors.transparent,
|
||
border: Border(
|
||
bottom: BorderSide(color: const Color(0xFFF0F0F0), width: 0.5),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 2,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(name, style: TextStyle(fontSize: 13, color: isAbnormal ? const Color(0xFFE53935) : const Color(0xFF333333), fontWeight: isAbnormal ? FontWeight.w600 : FontWeight.normal)),
|
||
if (refRange.isNotEmpty) Text('参考:$refRange', style: const TextStyle(fontSize: 10, color: Color(0xFFAAAAAA))),
|
||
],
|
||
),
|
||
),
|
||
Expanded(flex: 1, child: Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: sc))),
|
||
Expanded(
|
||
flex: 1,
|
||
child: Center(
|
||
child: Container(
|
||
width: 8,
|
||
height: 8,
|
||
decoration: BoxDecoration(shape: BoxShape.circle, color: sc),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
),
|
||
|
||
// AI 解读摘要
|
||
if (summary.isNotEmpty) ...[
|
||
const SizedBox(height: 14),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF3EFFF),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFDDD8FF), width: 0.8),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Row(
|
||
children: [
|
||
Icon(Icons.auto_awesome, size: 16, color: Color(0xFF635BFF)),
|
||
SizedBox(width: 6),
|
||
Text('AI 解读摘要', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF635BFF))),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(summary, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.5)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
|
||
// 查看完整解读按钮
|
||
const SizedBox(height: 14),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: _cardFilledBtn('查看完整解读', Icons.article_outlined),
|
||
),
|
||
const SizedBox(height: 4),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 6. QuickOptionsCard — 优化样式
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildQuickOptionsCard(BuildContext context, ChatMessage msg) {
|
||
final meta = msg.metadata;
|
||
final options = meta?['options'] as List? ?? [];
|
||
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFFFFF),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
|
||
),
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 14),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(msg.content, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A2E))),
|
||
const SizedBox(height: 14),
|
||
Wrap(spacing: 8, runSpacing: 8, children: options.map((opt) {
|
||
final o = opt as Map? ?? {};
|
||
return ElevatedButton(
|
||
onPressed: () {},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFF5F3FF),
|
||
foregroundColor: const Color(0xFF635BFF),
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11),
|
||
),
|
||
child: Text(o['label'] as String? ?? '', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
||
);
|
||
}).toList()),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 公共组件:思考气泡 & 文本气泡
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _buildThinkingBubble(BuildContext context, String? thinkingText) {
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFEFEFF),
|
||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
|
||
border: Border.all(color: const Color(0xFFE8E6FF), width: 1.5),
|
||
boxShadow: [BoxShadow(color: const Color(0xFF635BFF).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 26,
|
||
height: 26,
|
||
padding: const EdgeInsets.all(5),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFEDEBFF),
|
||
borderRadius: BorderRadius.circular(13),
|
||
),
|
||
child: const CircularProgressIndicator(strokeWidth: 2.2, color: Color(0xFF635BFF)),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTextBubble(BuildContext context, ChatMessage msg) {
|
||
final isUser = msg.isUser;
|
||
return Align(
|
||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
decoration: BoxDecoration(
|
||
color: isUser ? const Color(0xFF635BFF) : const Color(0xFFFEFEFF),
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: Radius.circular(isUser ? 20 : 4),
|
||
topRight: Radius.circular(isUser ? 4 : 20),
|
||
bottomLeft: const Radius.circular(20),
|
||
bottomRight: const Radius.circular(20),
|
||
),
|
||
border: isUser ? null : Border.all(color: const Color(0xFFE8E6FF), width: 1.5),
|
||
boxShadow: isUser ? [] : [BoxShadow(color: const Color(0xFF635BFF).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (isUser)
|
||
Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4))
|
||
else
|
||
MarkdownBody(
|
||
data: msg.content,
|
||
selectable: true,
|
||
styleSheet: MarkdownStyleSheet(
|
||
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5),
|
||
h1: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||
h2: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
|
||
code: const TextStyle(fontSize: 14, backgroundColor: Color(0xFFF5F3FF)),
|
||
),
|
||
),
|
||
if (!isUser && msg.content.isNotEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 10),
|
||
child: Row(children: [
|
||
const CircleAvatar(radius: 10, backgroundColor: Color(0xFFEDEBFF), child: Icon(Icons.chat_bubble_outline, size: 14, color: Color(0xFF635BFF))),
|
||
const SizedBox(width: 6),
|
||
const Text('健康管家', style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
|
||
const SizedBox(width: 4),
|
||
const Text('仅供参考', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))),
|
||
]),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 公共组件:通用按钮
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
Widget _cardFilledBtn(String label, IconData icon) {
|
||
return ElevatedButton(
|
||
onPressed: () {},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF635BFF),
|
||
foregroundColor: Colors.white,
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||
),
|
||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||
Icon(icon, size: 16),
|
||
const SizedBox(width: 5),
|
||
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
|
||
]),
|
||
);
|
||
}
|
||
|
||
Widget _cardOutlineBtn(String label, IconData icon) {
|
||
return OutlinedButton(
|
||
onPressed: () {},
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: const Color(0xFF635BFF),
|
||
side: const BorderSide(color: Color(0xFF635BFF), width: 1.2),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||
),
|
||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||
Icon(icon, size: 15),
|
||
const SizedBox(width: 4),
|
||
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
|
||
]),
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 工具方法
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
String _formatTime(DateTime dt) {
|
||
final now = DateTime.now();
|
||
final today = DateTime(now.year, now.month, now.day);
|
||
final thatDay = DateTime(dt.year, dt.month, dt.day);
|
||
if (thatDay == today) {
|
||
return '今天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||
}
|
||
if (thatDay == today.subtract(const Duration(days: 1))) {
|
||
return '昨天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||
}
|
||
return '${dt.month}/${dt.day} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||
}
|
||
|
||
String _getMetricUnit(String type) {
|
||
switch (type.toLowerCase()) {
|
||
case 'blood_pressure': return 'mmHg';
|
||
case 'heart_rate': return 'bpm';
|
||
case 'glucose': return 'mmol/L';
|
||
case 'spo2': return '%';
|
||
case 'weight': return 'kg';
|
||
default: return '';
|
||
}
|
||
}
|
||
|
||
String _getMetricIcon(String type) {
|
||
switch (type.toLowerCase()) {
|
||
case 'blood_pressure': return '🩺';
|
||
case 'heart_rate': return '💓';
|
||
case 'glucose': return '💉';
|
||
case 'spo2': return '🫁';
|
||
case 'weight': return '⚖️';
|
||
default: return '📊';
|
||
}
|
||
}
|
||
|
||
String _getMetricName(String type) {
|
||
switch (type.toLowerCase()) {
|
||
case 'blood_pressure': return '血压';
|
||
case 'heart_rate': return '心率';
|
||
case 'glucose': return '血糖';
|
||
case 'spo2': return '血氧';
|
||
case 'weight': return '体重';
|
||
default: return '健康指标';
|
||
}
|
||
}
|
||
|
||
static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) {
|
||
return switch (agent) {
|
||
ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'),
|
||
ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'),
|
||
ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'),
|
||
ActiveAgent.consultation => (Icons.local_hospital, '问诊', '在线咨询医生,描述症状获取建议'),
|
||
ActiveAgent.report => (Icons.assignment, '看报告', '上传体检报告,AI 辅助解读'),
|
||
ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'),
|
||
_ => (Icons.smart_toy, 'AI 助手', '您的智能健康管家'),
|
||
};
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// 内部数据类
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
typedef _AgentIcon = IconData;
|
||
|
||
class _AgentAction {
|
||
final String label;
|
||
final IconData icon;
|
||
final bool isWide;
|
||
|
||
const _AgentAction({required this.label, required this.icon, this.isWide = false});
|
||
}
|
||
|
||
final _agentActions = <ActiveAgent, List<_AgentAction>>{
|
||
ActiveAgent.health: [
|
||
_AgentAction(label: '录入血压', icon: Icons.monitor_heart_outlined),
|
||
_AgentAction(label: '录入血糖', icon: Icons.bloodtype_outlined),
|
||
_AgentAction(label: '录入心率', icon: Icons.favorite_border),
|
||
_AgentAction(label: '录入血氧', icon: Icons.air_outlined),
|
||
_AgentAction(label: '录入体重', icon: Icons.monitor_weight_outlined),
|
||
],
|
||
ActiveAgent.diet: [
|
||
_AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true),
|
||
_AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true),
|
||
_AgentAction(label: '看舌答', icon: Icons.face_retouching_natural_outlined, isWide: true),
|
||
_AgentAction(label: '测肤质', icon: Icons.palette_outlined, isWide: true),
|
||
],
|
||
ActiveAgent.medication: [
|
||
_AgentAction(label: '用药管理', icon: Icons.medication_liquid_outlined, isWide: true),
|
||
_AgentAction(label: '用药提醒', icon: Icons.alarm_outlined, isWide: true),
|
||
_AgentAction(label: '添加药品', icon: Icons.add_circle_outline, isWide: true),
|
||
],
|
||
ActiveAgent.consultation: [
|
||
_AgentAction(label: '找医生', icon: Icons.person_search_outlined, isWide: true),
|
||
_AgentAction(label: '描述症状', icon: Icons.edit_note_outlined, isWide: true),
|
||
],
|
||
ActiveAgent.report: [
|
||
_AgentAction(label: '上传报告', icon: Icons.upload_file_outlined, isWide: true),
|
||
_AgentAction(label: '查看历史', icon: Icons.history_outlined, isWide: true),
|
||
],
|
||
ActiveAgent.exercise: [
|
||
_AgentAction(label: '本周计划', icon: Icons.calendar_month_outlined, isWide: true),
|
||
_AgentAction(label: '新建计划', icon: Icons.add_task_outlined, isWide: true),
|
||
_AgentAction(label: '今日打卡', icon: Icons.fact_check_outlined, isWide: true),
|
||
],
|
||
};
|
||
|
||
extension _AgentActionsExt on ActiveAgent {
|
||
List<_AgentAction> get actions => _agentActions[this] ?? [const _AgentAction(label: '开始对话', icon: Icons.chat_outlined)];
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// 可展开的 AI 建议小组件
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
class _ExpandableAdvice extends StatefulWidget {
|
||
final String advice;
|
||
const _ExpandableAdvice({required this.advice});
|
||
|
||
@override
|
||
State<_ExpandableAdvice> createState() => _ExpandableAdviceState();
|
||
}
|
||
|
||
class _ExpandableAdviceState extends State<_ExpandableAdvice> {
|
||
bool _expanded = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GestureDetector(
|
||
onTap: () => setState(() => _expanded = !_expanded),
|
||
child: Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF7F5FF),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFE8E4FF), width: 0.8),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.lightbulb_outline, size: 16, color: Color(0xFF635BFF)),
|
||
const SizedBox(width: 6),
|
||
const Text('AI 建议', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF635BFF))),
|
||
const Spacer(),
|
||
Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 18, color: const Color(0xFFAAAAAA)),
|
||
],
|
||
),
|
||
if (_expanded) ...[
|
||
const SizedBox(height: 10),
|
||
Text(widget.advice, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.6)),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|