feat: 全面UI改造 — 报告/饮食/运动/用药/侧边栏/对话流/胶囊
- 报告模块:重写AI解读页,统一白色卡片+三栏布局(指标→解读→医生审核);加删除功能+后端删接口;VLM自动识别报告类型生成中文标题;去五颜六色
- 饮食分析:全面重设计,暖橙主题配色,图片自适应,餐次emoji选择器,去紫色
- 运动确认卡片:修复duration_minutes JSON类型转换,支持中文数字识别(三十分钟/半小时→30);主区域只显示运动名,字段行改为横向排列
- 流式输出:简化为最基础逐字追加,去buffer+Timer+淡入动画
- 欢迎卡片:每智能体独立渐变色(青/橙/蓝/紫/绿/粉),加卡片入场滑入+淡入动画(600ms)
- 确认卡片:头部去紫色背景改浅灰白,字段行去图标改纯信息横排,编辑框去双重边框
- 用药管理:胶囊改TabBar,添加按钮改右下FAB;打卡按钮改对号圆圈形式;药丸图标白底
- 侧边栏:加VIP服务/保险栏+图标,标题字体放大;服务包去查看更多+去VIP金色标签
- 胶囊:首页智能体胶囊白底深色字; side胶囊加阴影
- 对话流:reverse=false,今日健康出现在顶部; 对话页input bar匹配主页面样式
- 其他:个人资料去紫色+去多余入口;设置页白底图标;蓝牙页加返回按钮;报告管理改名
- 后端:加DELETE /api/reports/{id}; 修复manage_exercise数据类型转换;AI提示优化中文数字
This commit is contained in:
@@ -61,7 +61,9 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
final currentCount = chatState.messages.length;
|
||||
if (currentCount > _lastMsgCount) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollCtrl.hasClients) _scrollCtrl.jumpTo(0);
|
||||
if (_scrollCtrl.hasClients) {
|
||||
_scrollCtrl.jumpTo(_scrollCtrl.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
}
|
||||
_lastMsgCount = currentCount;
|
||||
@@ -88,10 +90,9 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
child: Row(children: [
|
||||
Builder(builder: (ctx) => GestureDetector(
|
||||
onTap: () => Scaffold.of(ctx).openDrawer(),
|
||||
child: Container(
|
||||
width: 38, height: 38,
|
||||
decoration: BoxDecoration(color: AppColors.iconBg, borderRadius: BorderRadius.circular(10)),
|
||||
child: const Icon(LucideIcons.menu, size: 20, color: AppColors.primary),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Icon(LucideIcons.menu, size: 22, color: AppColors.textPrimary),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
@@ -122,7 +123,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
(ActiveAgent.health, '记数据', LucideIcons.heart),
|
||||
(ActiveAgent.diet, '拍饮食', LucideIcons.utensils),
|
||||
(ActiveAgent.medication, '药管家', LucideIcons.pill),
|
||||
(ActiveAgent.report, '看报告', LucideIcons.fileText),
|
||||
(ActiveAgent.report, '报告分析', LucideIcons.fileText),
|
||||
(ActiveAgent.exercise, '运动', LucideIcons.trendingUp),
|
||||
];
|
||||
|
||||
@@ -139,16 +140,16 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
return GestureDetector(
|
||||
onTap: () => ref.read(chatProvider.notifier).triggerAgent(agent, label),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(icon, size: 15, color: AppColors.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primary)),
|
||||
Icon(icon, size: 17, color: AppColors.textPrimary),
|
||||
const SizedBox(width: 5),
|
||||
Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -194,7 +195,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
onTap: () => _showAttachmentPicker(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(bottom: 10),
|
||||
child: Icon(LucideIcons.paperclip, size: 24, color: AppColors.textSecondary),
|
||||
child: Icon(LucideIcons.plus, size: 24, color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -231,7 +232,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
width: 40, height: 40,
|
||||
margin: EdgeInsets.only(bottom: _pickedImagePath != null ? 0 : 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
color: AppColors.blueMeasure,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(LucideIcons.send, size: 20, color: Colors.white),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/app_colors.dart';
|
||||
import '../../../core/app_theme.dart';
|
||||
@@ -8,6 +9,28 @@ import '../../../providers/auth_provider.dart';
|
||||
import '../../../providers/chat_provider.dart';
|
||||
import '../../../providers/data_providers.dart';
|
||||
|
||||
/// 卡片入场动画包装(从下往上滑入 + 淡入)
|
||||
class _AnimatedCardEntry extends StatefulWidget {
|
||||
final Widget child;
|
||||
const _AnimatedCardEntry({required this.child});
|
||||
|
||||
@override State<_AnimatedCardEntry> createState() => _AnimatedCardEntryState();
|
||||
}
|
||||
|
||||
class _AnimatedCardEntryState extends State<_AnimatedCardEntry> with SingleTickerProviderStateMixin {
|
||||
late final _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 600));
|
||||
late final _slide = Tween<Offset>(begin: const Offset(0, 0.12), end: Offset.zero).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic));
|
||||
late final _fade = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
|
||||
|
||||
@override void initState() { super.initState(); _ctrl.forward(); }
|
||||
@override void dispose() { _ctrl.dispose(); super.dispose(); }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(opacity: _fade, child: SlideTransition(position: _slide, child: widget.child));
|
||||
}
|
||||
}
|
||||
|
||||
/// 对话消息列表
|
||||
class ChatMessagesView extends ConsumerWidget {
|
||||
final ScrollController scrollCtrl;
|
||||
@@ -44,11 +67,11 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
|
||||
return ListView.builder(
|
||||
controller: scrollCtrl,
|
||||
reverse: true,
|
||||
reverse: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final msg = messages[messages.length - 1 - index];
|
||||
final msg = messages[index];
|
||||
return _buildMessageContent(context, ref, msg, chatState);
|
||||
},
|
||||
);
|
||||
@@ -73,26 +96,6 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 逐字淡入文本(蚂蚁阿福风格)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
Widget _buildFadeInText(String text, TextStyle style, String stableId, {bool enabled = true}) {
|
||||
if (!enabled || text.length <= 2) return Text(text, style: style);
|
||||
|
||||
return Wrap(
|
||||
children: List.generate(text.length, (i) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
key: ValueKey('fade_${stableId}_$i'),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
builder: (_, value, child) => Opacity(opacity: value, child: child),
|
||||
child: Text(text[i], style: style),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 1. AgentWelcomeCard — 智能体欢迎卡片(蚂蚁阿福风格 + 美化版)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -102,15 +105,21 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
final actions = agent.actions;
|
||||
final features = _getAgentFeatures(agent);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final agentColors = _agentColors(agent);
|
||||
final iconGradient = LinearGradient(
|
||||
colors: agentColors.gradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
return Align(
|
||||
return _AnimatedCardEntry(child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
constraints: BoxConstraints(maxWidth: screenWidth * 0.95),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
@@ -125,8 +134,8 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.purpleGradientStart.withAlpha(40),
|
||||
AppColors.blueGradientEnd.withAlpha(30),
|
||||
agentColors.border,
|
||||
agentColors.bg,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
@@ -140,11 +149,11 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.purpleBlueGradient,
|
||||
gradient: iconGradient,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryPurple.withAlpha(30),
|
||||
color: agentColors.accent.withAlpha(77),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@@ -163,11 +172,12 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withAlpha(160),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.auto_awesome, size: 14, color: AppColors.iconColor),
|
||||
Icon(Icons.auto_awesome, size: 14, color: agentColors.accent),
|
||||
const SizedBox(width: 4),
|
||||
Text('AI 智能助手', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.iconColor)),
|
||||
Text('AI 智能助手', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: agentColors.accent)),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
@@ -184,67 +194,43 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
// ── 功能特性区 ──
|
||||
if (features.isNotEmpty) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundSecondary,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 6, height: 18,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.purpleBlueGradient,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
|
||||
child: features.any((f) => f.imageAsset != null)
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Image.asset(
|
||||
features.firstWhere((f) => f.imageAsset != null).imageAsset!,
|
||||
width: double.infinity, height: 170, fit: BoxFit.fill,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('核心功能', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: features.map((feature) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: features.indexOf(feature) == 0 ? 0 : 6,
|
||||
right: features.indexOf(feature) == features.length - 1 ? 0 : 6,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: AppColors.cardInner, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.border)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(width: 4, height: 16, decoration: BoxDecoration(color: agentColors.accent, borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(width: 8),
|
||||
const Text('核心功能', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textSecondary)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: features.map((feature) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 3),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(children: [
|
||||
Container(width: 40, height: 40, decoration: BoxDecoration(color: agentColors.iconBg, borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(feature.icon, size: 20, color: agentColors.accent)),
|
||||
const SizedBox(height: 6),
|
||||
Text(feature.title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
Text(feature.desc, style: const TextStyle(fontSize: 11, color: AppColors.textHint)),
|
||||
]),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.lightGradient,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(feature.icon, size: 24, color: AppColors.iconColor),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(feature.title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.textPrimary), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 2),
|
||||
Text(feature.desc, style: const TextStyle(fontSize: 13, color: AppColors.textHint), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList()),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -253,7 +239,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
const SizedBox(height: 14),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: _buildDoctorCards(ref),
|
||||
child: _buildDoctorCards(ref, agentColors),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -269,7 +255,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
Container(
|
||||
width: 6, height: 18,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.purpleBlueGradient,
|
||||
gradient: iconGradient,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
@@ -277,10 +263,13 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
Text('快捷操作', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: actions.map((a) => _buildActionButton(a, context, ref)).toList(),
|
||||
Row(
|
||||
children: [
|
||||
for (int i = 0; i < actions.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 10),
|
||||
Expanded(child: _buildActionButton(actions[i], context, ref, agentColors)),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -294,24 +283,24 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.iconBackground,
|
||||
color: agentColors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(children: [
|
||||
Icon(Icons.lightbulb_outline, size: 18, color: AppColors.iconColor),
|
||||
Icon(Icons.lightbulb_outline, size: 18, color: agentColors.accent),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text('直接描述您的需求,AI 会自动为您记录和分析', style: TextStyle(fontSize: 14, color: AppColors.iconColor, fontWeight: FontWeight.w500))),
|
||||
Expanded(child: Text('直接描述您的需求,AI 会自动为您记录和分析', style: TextStyle(fontSize: 14, color: agentColors.accent, fontWeight: FontWeight.w500))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(_AgentAction a, BuildContext context, WidgetRef ref) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
Widget _buildActionButton(_AgentAction a, BuildContext context, WidgetRef ref, _AgentColors colors) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (a.label == '服药打卡') {
|
||||
@@ -326,13 +315,12 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
width: ((screenWidth - 40 - 10) / 2) - 5,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.borderLight, width: 1.5),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
@@ -341,10 +329,10 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.lightGradient,
|
||||
color: colors.iconBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(a.icon, size: 22, color: AppColors.iconColor),
|
||||
child: Icon(a.icon, size: 22, color: colors.accent),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(a.label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary))),
|
||||
@@ -362,7 +350,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
_AgentFeature(Icons.add, '快速录入', '血压心率血糖'),
|
||||
],
|
||||
ActiveAgent.diet => [
|
||||
_AgentFeature(Icons.camera_alt, '拍照识别', '自动识别食物'),
|
||||
_AgentFeature(Icons.camera_alt, '拍照识别', '自动识别食物', imageAsset: 'assets/images/diet.png'),
|
||||
_AgentFeature(Icons.bar_chart, '营养分析', '热量营养成分'),
|
||||
],
|
||||
ActiveAgent.medication => [
|
||||
@@ -374,11 +362,11 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
_AgentFeature(Icons.local_hospital, '科室选择', '精准匹配'),
|
||||
],
|
||||
ActiveAgent.report => [
|
||||
_AgentFeature(Icons.upload, '上传报告', '支持多种格式'),
|
||||
_AgentFeature(Icons.upload, '上传报告', '支持多种格式', imageAsset: 'assets/images/Report Analysis.png'),
|
||||
_AgentFeature(Icons.search, 'AI解读', '智能分析报告'),
|
||||
],
|
||||
ActiveAgent.exercise => [
|
||||
_AgentFeature(Icons.calendar_month, '运动计划', '科学规划'),
|
||||
_AgentFeature(Icons.calendar_month, '运动计划', '科学规划', imageAsset: 'assets/images/sport.png'),
|
||||
_AgentFeature(Icons.check_circle, '打卡记录', '坚持完成'),
|
||||
],
|
||||
_ => [
|
||||
@@ -390,7 +378,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
|
||||
|
||||
|
||||
Widget _buildDoctorCards(WidgetRef ref) {
|
||||
Widget _buildDoctorCards(WidgetRef ref, _AgentColors colors) {
|
||||
const doctors = [
|
||||
{'name': '张明', 'title': '主任医师', 'dept': '心脏康复科', 'desc': '冠心病、高血压术后管理', 'id': '468b82e2-d95a-4436-bff6-a50eecf99a66'},
|
||||
{'name': '李芳', 'title': '副主任医师', 'dept': '营养科', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'd4148733-b538-4398-af17-0c7592fc0c2d'},
|
||||
@@ -400,13 +388,13 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
children: [
|
||||
for (var i = 0; i < doctors.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 8),
|
||||
Expanded(child: _doctorCard(doctors[i], ref)),
|
||||
Expanded(child: _doctorCard(doctors[i], ref, colors)),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _doctorCard(Map<String, String> doc, WidgetRef ref) {
|
||||
Widget _doctorCard(Map<String, String> doc, WidgetRef ref, _AgentColors colors) {
|
||||
return InkWell(
|
||||
onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -415,13 +403,13 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.primaryLight),
|
||||
border: Border.all(color: colors.border),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: AppTheme.primaryLight,
|
||||
child: Text(doc['name']![0], style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppTheme.primary)),
|
||||
backgroundColor: colors.iconBg,
|
||||
child: Text(doc['name']![0], style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: colors.accent)),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(doc['name']!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
@@ -430,10 +418,10 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryLight,
|
||||
color: colors.iconBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(doc['dept']!, style: const TextStyle(fontSize: 13, color: AppTheme.primary)),
|
||||
child: Text(doc['dept']!, style: TextStyle(fontSize: 13, color: colors.accent)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
@@ -474,7 +462,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
List<_ConfirmField> fields = [];
|
||||
|
||||
if (isMedication) {
|
||||
// 药品录入
|
||||
// 药品录入 — 主展示区已显示药名+剂量,详情只列额外信息
|
||||
title = '药品录入确认';
|
||||
titleIcon = Icons.medication_liquid_outlined;
|
||||
mainLabel = meta['name'] as String? ?? '';
|
||||
@@ -482,59 +470,60 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
mainUnit = '';
|
||||
|
||||
if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt);
|
||||
final nameValue = meta['药品名称'] as String? ?? mainLabel;
|
||||
fields.add(_ConfirmField(label: '药品名称', value: nameValue, icon: Icons.medication_outlined, editable: true));
|
||||
if (mainValue.isNotEmpty) {
|
||||
final dosageValue = meta['剂量'] as String? ?? mainValue;
|
||||
fields.add(_ConfirmField(label: '剂量', value: dosageValue, icon: Icons.local_pharmacy_outlined, editable: true));
|
||||
}
|
||||
final time = meta['time'] as String? ?? '';
|
||||
if (time.isNotEmpty) {
|
||||
final timeValue = meta['服药时间'] as String? ?? time;
|
||||
fields.add(_ConfirmField(label: '服药时间', value: timeValue, icon: Icons.access_time, editable: true));
|
||||
fields.add(_ConfirmField(label: '服药时间', value: timeValue, editable: true));
|
||||
}
|
||||
final frequency = meta['frequency'] as String? ?? '';
|
||||
if (frequency.isNotEmpty) {
|
||||
final freqValue = meta['频率'] as String? ?? _freqLabel(frequency);
|
||||
fields.add(_ConfirmField(label: '频率', value: freqValue, icon: Icons.repeat_outlined, editable: true));
|
||||
fields.add(_ConfirmField(label: '频率', value: freqValue, editable: true));
|
||||
}
|
||||
final duration = meta['duration_days'] as int?;
|
||||
if (duration != null && duration > 0) {
|
||||
final durationValue = meta['服用天数'] as String? ?? '$duration 天';
|
||||
fields.add(_ConfirmField(label: '服用天数', value: durationValue, icon: Icons.event_available_outlined, editable: true));
|
||||
fields.add(_ConfirmField(label: '服用天数', value: durationValue, editable: true));
|
||||
}
|
||||
} else if (isExercise) {
|
||||
// 运动计划
|
||||
// 运动计划 — 主展示区大号显示运动名,字段列时长/频率/天数
|
||||
title = '运动计划确认';
|
||||
titleIcon = Icons.directions_run_outlined;
|
||||
mainLabel = meta['name'] as String? ?? meta['activity'] as String? ?? '运动计划';
|
||||
mainValue = meta['duration'] as String? ?? '';
|
||||
mainUnit = meta['duration_unit'] as String? ?? '分钟';
|
||||
final exName = (meta['value'] ?? meta['name'] ?? meta['exerciseType'] ?? meta['运动类型'] ?? '').toString();
|
||||
final exUnitRaw = (meta['unit'] ?? meta['duration_unit'] ?? meta['durationUnit'] ?? '').toString();
|
||||
final exDaysRaw = meta['durationDays'] ?? meta['day_count'];
|
||||
final exDays = int.tryParse(exDaysRaw?.toString() ?? '') ?? 0;
|
||||
|
||||
// 解析 "每天30分钟" → 频率=每天, 时长=30分钟
|
||||
final freqMatch = RegExp(r'^(每天|每周|每月)').firstMatch(exUnitRaw);
|
||||
final durMatch = RegExp(r'(\d+分钟)').firstMatch(exUnitRaw);
|
||||
final freq = freqMatch?.group(1) ?? '';
|
||||
final dur = durMatch?.group(1) ?? exUnitRaw;
|
||||
|
||||
mainLabel = '运动计划';
|
||||
mainValue = exName.isNotEmpty ? exName : '运动计划';
|
||||
mainUnit = '';
|
||||
|
||||
if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt);
|
||||
final typeValue = meta['运动类型'] as String? ?? mainLabel;
|
||||
fields.add(_ConfirmField(label: '运动类型', value: typeValue, icon: Icons.fitness_center_outlined, editable: true));
|
||||
if (mainValue.isNotEmpty) {
|
||||
final durationValue = meta['运动时长'] as String? ?? '$mainValue $mainUnit';
|
||||
fields.add(_ConfirmField(label: '运动时长', value: durationValue, icon: Icons.timer_outlined, editable: true));
|
||||
if (dur.isNotEmpty) {
|
||||
fields.add(_ConfirmField(label: '运动时长', value: dur, editable: true));
|
||||
}
|
||||
final intensity = meta['intensity'] as String? ?? '';
|
||||
if (freq.isNotEmpty) {
|
||||
fields.add(_ConfirmField(label: '频率', value: freq));
|
||||
}
|
||||
if (exDays > 0) {
|
||||
fields.add(_ConfirmField(label: '计划天数', value: '${exDays}天'));
|
||||
}
|
||||
final intensity = (meta['intensity'] ?? meta['运动强度'] ?? '').toString();
|
||||
if (intensity.isNotEmpty) {
|
||||
final intensityValue = meta['运动强度'] as String? ?? intensity;
|
||||
fields.add(_ConfirmField(label: '运动强度', value: intensityValue, icon: Icons.local_fire_department_outlined, editable: true));
|
||||
fields.add(_ConfirmField(label: '运动强度', value: intensity, editable: true));
|
||||
}
|
||||
final calories = meta['calories'] as String? ?? meta['calorie'] as String? ?? '';
|
||||
final calories = (meta['calories'] ?? meta['calorie'] ?? meta['消耗热量'] ?? '').toString();
|
||||
if (calories.isNotEmpty) {
|
||||
final caloriesValue = meta['消耗热量'] as String? ?? '$calories kcal';
|
||||
fields.add(_ConfirmField(label: '消耗热量', value: caloriesValue, icon: Icons.local_fire_department, editable: true));
|
||||
}
|
||||
final schedule = meta['schedule'] as String? ?? '';
|
||||
if (schedule.isNotEmpty) {
|
||||
final scheduleValue = meta['计划时间'] as String? ?? schedule;
|
||||
fields.add(_ConfirmField(label: '计划时间', value: scheduleValue, icon: Icons.calendar_month_outlined, editable: true));
|
||||
fields.add(_ConfirmField(label: '消耗热量', value: '$calories kcal', editable: true));
|
||||
}
|
||||
} else {
|
||||
// 健康指标
|
||||
// 健康指标 — 主展示区已显示指标+数值+单位,详情只列额外信息
|
||||
title = '健康数据确认';
|
||||
titleIcon = Icons.monitor_heart_outlined;
|
||||
mainLabel = _getMetricName(backendType);
|
||||
@@ -542,25 +531,23 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
mainUnit = meta['unit'] as String? ?? _getMetricUnit(backendType);
|
||||
|
||||
if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt);
|
||||
final metricValue = meta[mainLabel] as String? ?? '$mainValue $mainUnit';
|
||||
fields.add(_ConfirmField(label: mainLabel, value: metricValue, icon: Icons.favorite_outline, editable: true));
|
||||
final timeValue = meta['记录时间'] as String? ?? recordTime;
|
||||
fields.add(_ConfirmField(label: '记录时间', value: timeValue, icon: Icons.access_time, editable: true));
|
||||
fields.add(_ConfirmField(label: '记录时间', value: timeValue, editable: true));
|
||||
final note = meta['note'] as String? ?? '';
|
||||
if (note.isNotEmpty) {
|
||||
final noteValue = meta['备注'] as String? ?? note;
|
||||
fields.add(_ConfirmField(label: '备注', value: noteValue, icon: Icons.edit_note_outlined, editable: true));
|
||||
fields.add(_ConfirmField(label: '备注', value: noteValue, editable: true));
|
||||
}
|
||||
}
|
||||
|
||||
return Align(
|
||||
return _AnimatedCardEntry(child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
constraints: BoxConstraints(maxWidth: screenWidth * 0.95),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
@@ -572,15 +559,8 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 18),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.purpleGradientStart.withAlpha(45),
|
||||
AppColors.blueGradientEnd.withAlpha(25),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF8F6FF),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -589,7 +569,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.purpleBlueGradient,
|
||||
color: AppColors.cardInner,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(color: AppColors.primaryPurple.withAlpha(35), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
@@ -655,31 +635,33 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(mainLabel, style: const TextStyle(fontSize: 17, color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: mainValue.isNotEmpty ? mainValue : '—',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: abnormal ? AppColors.error : AppColors.textPrimary,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
if (mainUnit.isNotEmpty)
|
||||
if (mainValue.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' $mainUnit',
|
||||
text: mainValue,
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: abnormal ? AppColors.error : AppColors.iconColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: abnormal ? AppColors.error : AppColors.textPrimary,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (mainUnit.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' $mainUnit',
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: abnormal ? AppColors.error : AppColors.iconColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -724,7 +706,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Column(
|
||||
@@ -748,7 +730,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.borderLight, width: 1.5),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
@@ -756,7 +738,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.successButtonGradient,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: AppColors.buttonShadow,
|
||||
),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
@@ -771,7 +753,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
onTap: () {
|
||||
ref.read(chatProvider.notifier).confirmMessage(msg.id);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -795,6 +777,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -802,40 +785,27 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
final isEditing = field.label == msg.metadata?['_editingField'];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 42, height: 42,
|
||||
decoration: BoxDecoration(color: AppColors.iconBackground, borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(field.icon, size: 22, color: AppColors.iconColor),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(field.label, style: const TextStyle(fontSize: 15, color: AppColors.textHint)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(field.label, style: const TextStyle(fontSize: 15, color: AppColors.textHint)),
|
||||
const SizedBox(height: 4),
|
||||
if (!field.editable)
|
||||
Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary))
|
||||
else if (isEditing)
|
||||
_buildEditableTextField(field, msg, ref)
|
||||
else
|
||||
InkWell(
|
||||
onTap: () {
|
||||
ref.read(chatProvider.notifier).startEditingField(msg.id, field.label);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary))),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.edit_outlined, size: 18, color: AppColors.border),
|
||||
],
|
||||
),
|
||||
child: !field.editable
|
||||
? Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textPrimary))
|
||||
: isEditing
|
||||
? _buildEditableTextField(field, msg, ref)
|
||||
: InkWell(
|
||||
onTap: () => ref.read(chatProvider.notifier).startEditingField(msg.id, field.label),
|
||||
child: Row(children: [
|
||||
Expanded(child: Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textPrimary))),
|
||||
Icon(Icons.edit_outlined, size: 18, color: AppColors.border),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -846,28 +816,30 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final controller = TextEditingController(text: field.value);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.primaryBlue.withAlpha(150), width: 1.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: AppColors.backgroundSecondary,
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: InputBorder.none,
|
||||
return TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: AppColors.primary.withAlpha(80)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, value);
|
||||
},
|
||||
onTapOutside: (_) {
|
||||
ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, controller.text);
|
||||
},
|
||||
),
|
||||
);
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -941,11 +913,17 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
// 文字内容
|
||||
if (isUser)
|
||||
SelectableText(msg.content, style: const TextStyle(fontSize: 19, color: Colors.white, height: 1.5))
|
||||
else if (chatState?.isStreaming ?? false)
|
||||
// 流式输出中:逐字淡入
|
||||
_buildFadeInText(_stripMd(msg.content), const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5), msg.id)
|
||||
else
|
||||
SelectableText(_stripMd(msg.content), style: const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5)),
|
||||
MarkdownBody(
|
||||
data: _cleanAiText(msg.content),
|
||||
selectable: true,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5),
|
||||
strong: const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5, fontWeight: FontWeight.w700),
|
||||
h1: const TextStyle(fontSize: 21, color: AppColors.textPrimary, fontWeight: FontWeight.w700),
|
||||
h2: const TextStyle(fontSize: 20, color: AppColors.textPrimary, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
|
||||
// 图片缩略图(在文字下方)
|
||||
if (hasImage)
|
||||
@@ -1051,16 +1029,10 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 去掉 AI 返回的 Markdown 标记(**加粗**、*斜体*、`代码`、标题、列表符号等)
|
||||
static String _stripMd(String text) {
|
||||
/// 清理 AI 返回文本中的异常占位符(Markdown 格式交给 MarkdownBody 渲染)
|
||||
static String _cleanAiText(String text) {
|
||||
return text
|
||||
.replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1')
|
||||
.replaceAll(RegExp(r'\*(.+?)\*'), r'$1')
|
||||
.replaceAll(RegExp(r'^#{1,6}\s+', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'^[\-\*]\s+', multiLine: true), '')
|
||||
.replaceAll(RegExp(r'`(.+?)`'), r'$1')
|
||||
.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1')
|
||||
.replaceAll('\$1', '') // AI 偶尔输出 $1 占位符
|
||||
.replaceAll('\$1', '') // AI 偶尔输出 $1 占位符(正则残留)
|
||||
.replaceAll(RegExp(r'\n{3,}'), '\n\n')
|
||||
.trim();
|
||||
}
|
||||
@@ -1181,11 +1153,11 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
accent: const Color(0xFFF0A060),
|
||||
),
|
||||
ActiveAgent.medication => _AgentColors(
|
||||
gradient: [const Color(0xFFFFD4E0), const Color(0xFFFFB8CC), const Color(0xFFE898A8)],
|
||||
bg: const Color(0xFFFFF0F4),
|
||||
border: const Color(0xFFFFE0E8),
|
||||
iconBg: const Color(0xFFFFE8EE),
|
||||
accent: const Color(0xFFE898A8),
|
||||
gradient: [const Color(0xFFB8E0D8), const Color(0xFF8ED4C8), const Color(0xFF68B8AC)],
|
||||
bg: const Color(0xFFF0FAF8),
|
||||
border: const Color(0xFFD4ECE8),
|
||||
iconBg: const Color(0xFFE4F6F2),
|
||||
accent: const Color(0xFF68B8AC),
|
||||
),
|
||||
ActiveAgent.report => _AgentColors(
|
||||
gradient: [const Color(0xFFD8D0F0), const Color(0xFFC4B8EC), const Color(0xFFA898D8)],
|
||||
@@ -1217,7 +1189,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'),
|
||||
ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'),
|
||||
ActiveAgent.consultation => (Icons.local_hospital, '问诊', '在线咨询医生,描述症状获取建议'),
|
||||
ActiveAgent.report => (Icons.assignment, '看报告', '上传体检报告,AI 辅助解读'),
|
||||
ActiveAgent.report => (Icons.assignment, '报告分析', '上传体检报告,AI 辅助解读'),
|
||||
ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'),
|
||||
_ => (Icons.smart_toy, 'AI 助手', '您的智能健康管家'),
|
||||
};
|
||||
@@ -1249,7 +1221,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
final now = DateTime.now();
|
||||
final tasks = <Widget>[];
|
||||
|
||||
// 1. 健康数据摘要行
|
||||
// ── 解析健康指标 ──
|
||||
final bp = healthData['BloodPressure'];
|
||||
final bpText = bp is Map ? '${bp['systolic'] ?? '--'}/${bp['diastolic'] ?? '--'}' : null;
|
||||
final hr = healthData['HeartRate'];
|
||||
@@ -1260,20 +1232,75 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
final boText = bo is num ? '$bo' : null;
|
||||
final wt = healthData['Weight'];
|
||||
final wtText = wt is num ? '$wt' : null;
|
||||
|
||||
final allNull = bpText == null && hrText == null && bsText == null && boText == null && wtText == null;
|
||||
|
||||
if (!allNull) {
|
||||
tasks.add(_taskRow(context, Icons.check_circle, '今日已记录', trailing: [
|
||||
if (bpText != null) '血压 $bpText',
|
||||
if (hrText != null) '心率 $hrText',
|
||||
if (bsText != null) '血糖 $bsText',
|
||||
if (boText != null) '血氧 $boText',
|
||||
if (wtText != null) '体重 $wtText',
|
||||
].join(' · '), status: 'done', onTap: () => pushRoute(ref, 'trend')));
|
||||
// 异常检测
|
||||
final bpAbnormal = bp is Map && bp['systolic'] is int && (bp['systolic'] as int) >= 140;
|
||||
final hasAbnormal = bpAbnormal;
|
||||
|
||||
// ── 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 {
|
||||
final parts = <String>[];
|
||||
if (bpText != null) parts.add('血压 $bpText');
|
||||
if (hrText != null) parts.add('心率 $hrText');
|
||||
if (bsText != null) parts.add('血糖 $bsText');
|
||||
if (boText != null) parts.add('血氧 $boText');
|
||||
if (wtText != null) parts.add('体重 $wtText');
|
||||
tasks.add(_taskRow(context, Icons.check_circle, '健康指标',
|
||||
trailing: parts.join(' · '),
|
||||
status: hasAbnormal ? 'warning' : 'done',
|
||||
iconColor: healthIconColor, iconBg: healthIconBg,
|
||||
onTap: () => pushRoute(ref, 'trend')));
|
||||
}
|
||||
if (bpAbnormal) {
|
||||
final s = bp['systolic'];
|
||||
final d = bp['diastolic'] ?? '--';
|
||||
tasks.add(_taskRow(context, Icons.warning_amber_rounded, '血压 $s/$d 偏高,请关注',
|
||||
status: 'warning',
|
||||
onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'})));
|
||||
}
|
||||
|
||||
// 2. 用药提醒 — 一行汇总
|
||||
// ── 2. 运动 ──
|
||||
const exIconColor = Color(0xFFF59E0B);
|
||||
const exIconBg = Color(0xFFFEF3C7);
|
||||
final exercisePlan = ref.watch(currentExercisePlanProvider);
|
||||
var hasExercise = false;
|
||||
exercisePlan.whenOrNull(data: (plan) {
|
||||
if (plan != null) {
|
||||
final items = (plan['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
final today = now.weekday % 7;
|
||||
final todayItem = items.cast<Map<String, dynamic>?>().firstWhere(
|
||||
(i) => i?['dayOfWeek'] == today, orElse: () => null);
|
||||
if (todayItem != null && todayItem['isRestDay'] != true) {
|
||||
hasExercise = true;
|
||||
final done = todayItem['isCompleted'] == true;
|
||||
final name = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动';
|
||||
final dur = items.isNotEmpty ? (items.first['durationMinutes']?.toString() ?? '30') : '30';
|
||||
tasks.add(_taskRow(context, Icons.directions_run, '$name $dur分钟',
|
||||
status: done ? 'done' : (now.hour >= 18 ? 'overdue' : 'pending'),
|
||||
iconColor: exIconColor, iconBg: exIconBg,
|
||||
onTap: () => pushRoute(ref, 'exercisePlan'),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!hasExercise) {
|
||||
tasks.add(_taskRow(context, Icons.directions_run, '运动',
|
||||
trailing: '暂无今日运动计划',
|
||||
status: 'pending', iconColor: exIconColor, iconBg: exIconBg,
|
||||
onTap: () => pushRoute(ref, 'exercisePlan')));
|
||||
}
|
||||
|
||||
// ── 3. 用药打卡 ──
|
||||
const medIconColor = Color(0xFFEC4899);
|
||||
const medIconBg = Color(0xFFFCE7F3);
|
||||
reminders.whenOrNull(data: (meds) {
|
||||
final overdue = meds.where((m) => m['status'] == 'overdue').length;
|
||||
final upcoming = meds.where((m) => m['status'] == 'upcoming').length;
|
||||
@@ -1284,57 +1311,27 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
if (overdue > 0) parts.add('$overdue项过期');
|
||||
if (upcoming > 0) parts.add('$upcoming项待服');
|
||||
if (taken > 0) parts.add('$taken项已服');
|
||||
tasks.add(_taskRow(
|
||||
context, Icons.medication_rounded, '用药打卡 ($total项)',
|
||||
tasks.add(_taskRow(context, Icons.medication_rounded, '用药打卡 ($total项)',
|
||||
trailing: parts.join(' · '),
|
||||
status: overdue > 0 ? 'overdue' : upcoming > 0 ? 'pending' : 'done',
|
||||
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, '暂无用药提醒', status: 'pending'));
|
||||
}
|
||||
|
||||
// 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 && 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';
|
||||
tasks.add(_taskRow(context, Icons.directions_run,
|
||||
'$name $dur分钟',
|
||||
status: done ? 'done' : (now.hour >= 18 ? 'overdue' : 'pending'),
|
||||
onTap: () => pushRoute(ref, 'exercisePlan'),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 异常指标
|
||||
if (bp is Map) {
|
||||
final s = bp['systolic'];
|
||||
if (s is int && s >= 140) {
|
||||
tasks.add(_taskRow(
|
||||
context, Icons.warning_amber_rounded, '血压 $s/${bp['diastolic'] ?? '--'} 偏高',
|
||||
status: 'warning',
|
||||
onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}),
|
||||
));
|
||||
}
|
||||
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),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: Color(0xFFE2E8F0), width: 2),
|
||||
boxShadow: AppColors.cardShadow,
|
||||
),
|
||||
@@ -1365,10 +1362,12 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap}) {
|
||||
Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap, Color? iconColor, Color? iconBg}) {
|
||||
final colors = {'done': AppTheme.success, 'warning': AppColors.warning, 'pending': AppColors.textHint, 'overdue': AppTheme.error};
|
||||
final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined, 'overdue': Icons.error};
|
||||
final isOverdue = status == 'overdue';
|
||||
final ic = iconColor ?? AppTheme.primary;
|
||||
final ibg = iconBg ?? AppTheme.primaryLight;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: GestureDetector(
|
||||
@@ -1381,7 +1380,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
|
||||
Container(width: 30, height: 30, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 18, color: AppTheme.primary)),
|
||||
Container(width: 30, height: 30, decoration: BoxDecoration(color: ibg, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 18, color: ic)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 16, color: AppColors.textPrimary))),
|
||||
Icon(icons[status] ?? Icons.circle_outlined, size: 21, color: colors[status] ?? Colors.grey),
|
||||
@@ -1427,8 +1426,9 @@ class _AgentFeature {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String desc;
|
||||
final String? imageAsset;
|
||||
|
||||
const _AgentFeature(this.icon, this.title, this.desc);
|
||||
const _AgentFeature(this.icon, this.title, this.desc, {this.imageAsset});
|
||||
}
|
||||
|
||||
class _ConfirmField {
|
||||
@@ -1446,7 +1446,7 @@ class _ConfirmField {
|
||||
|
||||
final _agentActions = <ActiveAgent, List<_AgentAction>>{
|
||||
ActiveAgent.health: [
|
||||
_AgentAction(label: '查看趋势', icon: Icons.trending_up, isWide: true, route: 'trend'),
|
||||
_AgentAction(label: '查看健康情况', icon: Icons.trending_up, isWide: true, route: 'trend'),
|
||||
],
|
||||
ActiveAgent.diet: [
|
||||
_AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, action: 'pickFoodCamera'),
|
||||
|
||||
Reference in New Issue
Block a user