diff --git a/health_app/lib/core/api_client.dart b/health_app/lib/core/api_client.dart index c1f6812..60851d9 100644 --- a/health_app/lib/core/api_client.dart +++ b/health_app/lib/core/api_client.dart @@ -3,7 +3,7 @@ import 'package:dio/dio.dart'; import 'local_database.dart'; /// API 基础地址 -const String baseUrl = 'http://10.4.164.158:5000'; +const String baseUrl = 'http://localhost:5000'; /// Dio HTTP 客户端封装——带 token 注入、401 自动刷新 class ApiClient { diff --git a/health_app/lib/pages/auth/login_page.dart b/health_app/lib/pages/auth/login_page.dart index 715a3dc..c05f209 100644 --- a/health_app/lib/pages/auth/login_page.dart +++ b/health_app/lib/pages/auth/login_page.dart @@ -92,30 +92,40 @@ class _LoginPageState extends ConsumerState { // 手机号 Container( - height: 52, decoration: BoxDecoration( - color: AppColors.backgroundSoft, + gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(10), ), - child: Row(children: [ - const Padding(padding: EdgeInsets.only(left: 16), child: Text('+86', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.textPrimary))), - Container(width: 1, height: 24, color: AppColors.border, margin: const EdgeInsets.symmetric(horizontal: 12)), - Expanded( - child: TextField( - controller: _phoneCtrl, - keyboardType: TextInputType.phone, - maxLength: 11, - style: const TextStyle(fontSize: 16, color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: '请输入手机号', - hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint), - border: InputBorder.none, - counterText: '', - contentPadding: EdgeInsets.symmetric(vertical: 14), + padding: const EdgeInsets.all(1.5), + child: Container( + height: 52, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(9), + ), + child: Row(children: [ + const Padding(padding: EdgeInsets.only(left: 16), child: Text('+86', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.textPrimary))), + Container(width: 1, height: 24, color: AppColors.border, margin: const EdgeInsets.symmetric(horizontal: 12)), + Expanded( + child: TextField( + controller: _phoneCtrl, + keyboardType: TextInputType.phone, + maxLength: 11, + style: const TextStyle(fontSize: 16, color: AppColors.textPrimary), + decoration: const InputDecoration( + filled: false, + hintText: '请输入手机号', + hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + counterText: '', + contentPadding: EdgeInsets.symmetric(vertical: 14), + ), ), ), - ), - ]), + ]), + ), ), const SizedBox(height: 16), @@ -123,22 +133,32 @@ class _LoginPageState extends ConsumerState { Row(children: [ Expanded( child: Container( - height: 52, decoration: BoxDecoration( - color: AppColors.backgroundSoft, + gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(10), ), - child: TextField( - controller: _codeCtrl, - keyboardType: TextInputType.number, - maxLength: 6, - style: const TextStyle(fontSize: 16, color: AppColors.textPrimary), - decoration: const InputDecoration( - hintText: '验证码', - hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint), - border: InputBorder.none, - counterText: '', - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + padding: const EdgeInsets.all(1.5), + child: Container( + height: 52, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(9), + ), + child: TextField( + controller: _codeCtrl, + keyboardType: TextInputType.number, + maxLength: 6, + style: const TextStyle(fontSize: 16, color: AppColors.textPrimary), + decoration: const InputDecoration( + filled: false, + hintText: '验证码', + hintStyle: TextStyle(fontSize: 15, color: AppColors.textHint), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + counterText: '', + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), ), ), ), @@ -150,7 +170,7 @@ class _LoginPageState extends ConsumerState { width: 120, height: 52, alignment: Alignment.center, decoration: BoxDecoration( - color: (_countdown > 0 || _sending) ? AppColors.backgroundSoft : AppColors.primary, + color: (_countdown > 0 || _sending) ? Colors.white : AppColors.primary, borderRadius: BorderRadius.circular(10), ), child: Text( @@ -170,7 +190,7 @@ class _LoginPageState extends ConsumerState { Container( width: 18, height: 18, margin: const EdgeInsets.only(right: 6), decoration: BoxDecoration( - color: _agreed ? AppColors.primary : Colors.transparent, + color: _agreed ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(4), border: Border.all(color: _agreed ? AppColors.primary : AppColors.border, width: 1.5), ), @@ -196,12 +216,13 @@ class _LoginPageState extends ConsumerState { child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( - gradient: AppColors.primaryGradient, + color: Colors.white, borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.primary, width: 1.5), ), child: _loading - ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white)) - : const Text('登 录', style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.w600, letterSpacing: 2)), + ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: AppColors.primary)) + : const Text('登 录', style: TextStyle(fontSize: 18, color: AppColors.primary, fontWeight: FontWeight.w600, letterSpacing: 2)), ), ), diff --git a/health_app/lib/pages/home/home_page.dart b/health_app/lib/pages/home/home_page.dart index 7018579..9b9c1e0 100644 --- a/health_app/lib/pages/home/home_page.dart +++ b/health_app/lib/pages/home/home_page.dart @@ -232,7 +232,11 @@ class _HomePageState extends ConsumerState { width: 40, height: 40, margin: EdgeInsets.only(bottom: _pickedImagePath != null ? 0 : 4), decoration: BoxDecoration( - color: AppColors.blueMeasure, + gradient: const LinearGradient( + colors: [AppColors.blueMeasure, AppColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), borderRadius: BorderRadius.circular(20), ), child: const Icon(LucideIcons.send, size: 20, color: Colors.white), diff --git a/health_app/lib/pages/home/widgets/chat_messages_view.dart b/health_app/lib/pages/home/widgets/chat_messages_view.dart index 08fc981..ddc247f 100644 --- a/health_app/lib/pages/home/widgets/chat_messages_view.dart +++ b/health_app/lib/pages/home/widgets/chat_messages_view.dart @@ -196,11 +196,18 @@ class ChatMessagesView extends ConsumerWidget { Padding( 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, + ? Container( + decoration: BoxDecoration( + gradient: iconGradient, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.all(2), + child: ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.asset( + features.firstWhere((f) => f.imageAsset != null).imageAsset!, + width: double.infinity, height: 170, fit: BoxFit.fill, + ), ), ) : Container( @@ -289,7 +296,7 @@ class ChatMessagesView extends ConsumerWidget { child: Row(children: [ Icon(Icons.lightbulb_outline, size: 18, color: agentColors.accent), const SizedBox(width: 8), - Expanded(child: Text('直接描述您的需求,AI 会自动为您记录和分析', style: TextStyle(fontSize: 14, color: agentColors.accent, fontWeight: FontWeight.w500))), + Expanded(child: Text(_agentTip(agent), style: TextStyle(fontSize: 15, color: AppColors.textPrimary.withAlpha(180), fontWeight: FontWeight.w500))), ]), ), ), @@ -304,7 +311,7 @@ class ChatMessagesView extends ConsumerWidget { return InkWell( onTap: () { if (a.label == '服药打卡') { - _medicationCheckIn(ref, context); + pushRoute(ref, 'medCheckIn'); } else if (a.action == 'pickFoodCamera' || a.action == 'pickFoodGallery') { ref.read(dietActionProvider.notifier).trigger(a.action!); } else if (a.route != null) { @@ -346,7 +353,7 @@ class ChatMessagesView extends ConsumerWidget { List<_AgentFeature> _getAgentFeatures(ActiveAgent agent) { return switch (agent) { ActiveAgent.health => [ - _AgentFeature(Icons.trending_up, '趋势分析', '查看历史数据'), + _AgentFeature(Icons.trending_up, '趋势分析', '查看历史数据', imageAsset: 'assets/images/Data.png'), _AgentFeature(Icons.add, '快速录入', '血压心率血糖'), ], ActiveAgent.diet => [ @@ -354,7 +361,7 @@ class ChatMessagesView extends ConsumerWidget { _AgentFeature(Icons.bar_chart, '营养分析', '热量营养成分'), ], ActiveAgent.medication => [ - _AgentFeature(Icons.list, '用药清单', '管理所有药品'), + _AgentFeature(Icons.list, '用药清单', '管理所有药品', imageAsset: 'assets/images/Medicine.png'), _AgentFeature(Icons.alarm, '智能提醒', '按时服药提醒'), ], ActiveAgent.consultation => [ @@ -1195,6 +1202,16 @@ class ChatMessagesView extends ConsumerWidget { }; } + static String _agentTip(ActiveAgent agent) => switch (agent) { + ActiveAgent.health => '直接说出您的症状或数据,如"血压130/85,心率72"', + ActiveAgent.diet => '拍照或描述您吃了什么,如"早餐吃了两个鸡蛋一杯牛奶"', + ActiveAgent.medication => '说出药品名称和用法,如"每天早晚各一片阿司匹林"', + ActiveAgent.consultation => '描述您的症状和不适感,如"最近总是头晕胸闷"', + ActiveAgent.report => '上传您的检查报告,AI 将自动提取指标并解读', + ActiveAgent.exercise => '说出运动计划,如"每天散步30分钟坚持一周"', + _ => '直接描述您的需求,AI 会自动为您记录和分析', + }; + static ActiveAgent _parseAgentFromName(String? name) { switch (name) { case 'consultation': return ActiveAgent.consultation; @@ -1246,25 +1263,20 @@ class ChatMessagesView extends ConsumerWidget { trailing: '今日暂无记录,点击录入', status: 'pending', iconColor: healthIconColor, iconBg: healthIconBg, onTap: () => pushRoute(ref, 'trend'))); - } else { - final parts = []; - 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, + } else if (hasAbnormal) { + // 有异常时显示异常指标 + final abnormalParts = []; + if (bpAbnormal) abnormalParts.add('血压 ${bp['systolic']}/${bp['diastolic']} 偏高'); + tasks.add(_taskRow(context, Icons.warning_amber_rounded, '健康指标', + trailing: abnormalParts.join(' · '), + status: 'warning', iconColor: healthIconColor, iconBg: healthIconBg, + onTap: () => pushRoute(ref, 'trend'))); + } else { + // 全部正常 + tasks.add(_taskRow(context, Icons.check_circle, '健康指标', + trailing: '指标正常', + status: '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. 运动 ── @@ -1302,21 +1314,29 @@ class ChatMessagesView extends ConsumerWidget { 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; - final taken = meds.where((m) => m['status'] == 'taken').length; - final total = overdue + upcoming + taken; - if (total > 0) { - final parts = []; - 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项)', - trailing: parts.join(' · '), - status: overdue > 0 ? 'overdue' : upcoming > 0 ? 'pending' : 'done', - iconColor: medIconColor, iconBg: medIconBg, - onTap: () => pushRoute(ref, 'medCheckIn'), - )); + if (meds.isNotEmpty) { + final overdueMeds = meds.where((m) => m['status'] == 'overdue').toList(); + final upcomingMeds = meds.where((m) => m['status'] == 'upcoming').toList(); + final takenMeds = meds.where((m) => m['status'] == 'taken').toList(); + // 未服/过期的在前面 + for (final m in [...overdueMeds, ...upcomingMeds]) { + final name = m['name']?.toString() ?? '药品'; + final time = m['scheduledTime']?.toString() ?? ''; + final isOverdue = m['status'] == 'overdue'; + tasks.add(_taskRow(context, Icons.medication_rounded, name, + trailing: '${isOverdue ? "❗过期" : "⏳待服"} $time', + status: isOverdue ? 'overdue' : 'pending', + iconColor: medIconColor, iconBg: medIconBg, + onTap: () => pushRoute(ref, 'medCheckIn'))); + } + // 已服的合并显示 + if (takenMeds.isNotEmpty) { + final names = takenMeds.map((m) => m['name']?.toString() ?? '药品').join('、'); + tasks.add(_taskRow(context, Icons.check_circle, names, + trailing: '已服用 (${takenMeds.length}项)', + status: 'done', + iconColor: medIconColor, iconBg: medIconBg)); + } } }); final hasMeds = reminders.asData?.value != null && (reminders.asData!.value).isNotEmpty; diff --git a/health_app/lib/widgets/app_menu_item.dart b/health_app/lib/widgets/app_menu_item.dart index 25dd290..711f337 100644 --- a/health_app/lib/widgets/app_menu_item.dart +++ b/health_app/lib/widgets/app_menu_item.dart @@ -22,45 +22,45 @@ class AppMenuItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 2), - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14), + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( - color: theme.colorScheme.card, - borderRadius: BorderRadius.circular(AppTheme.rSm), + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withAlpha(6), blurRadius: 8, offset: const Offset(0, 2))], ), child: Row(children: [ Container( - width: 38, - height: 38, + width: 40, + height: 40, decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(AppTheme.rSm), + color: AppColors.cardInner, + borderRadius: BorderRadius.circular(12), ), child: Icon(icon, size: 22, color: AppColors.textPrimary), ), - const SizedBox(width: AppTheme.sLg), + const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: TextStyle(fontSize: 19, fontWeight: FontWeight.w500, color: theme.colorScheme.foreground)), + Text(title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColors.textPrimary)), if (subtitle != null && subtitle!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), - child: Text(subtitle!, style: TextStyle(fontSize: 16, color: theme.colorScheme.mutedForeground)), + child: Text(subtitle!, style: const TextStyle(fontSize: 14, color: AppColors.textHint)), ), ], ), ), if (trailing != null && trailing!.isNotEmpty) - Text(trailing!, style: TextStyle(fontSize: 17, color: theme.colorScheme.mutedForeground)), + Text(trailing!, style: const TextStyle(fontSize: 15, color: AppColors.textHint)), const SizedBox(width: 4), - Icon(LucideIcons.chevronRight, size: 21, color: theme.colorScheme.mutedForeground), + const Icon(Icons.chevron_right, size: 20, color: AppColors.textHint), ]), ), );