diff --git a/health_app/assets/branding/agent_consultation_card.png b/health_app/assets/branding/agent_consultation_card.png new file mode 100644 index 0000000..f4d6297 Binary files /dev/null and b/health_app/assets/branding/agent_consultation_card.png differ diff --git a/health_app/assets/branding/agent_diet_card.png b/health_app/assets/branding/agent_diet_card.png new file mode 100644 index 0000000..84e2ff3 Binary files /dev/null and b/health_app/assets/branding/agent_diet_card.png differ diff --git a/health_app/assets/branding/agent_exercise_card.png b/health_app/assets/branding/agent_exercise_card.png new file mode 100644 index 0000000..a7a2280 Binary files /dev/null and b/health_app/assets/branding/agent_exercise_card.png differ diff --git a/health_app/assets/branding/agent_health_card.png b/health_app/assets/branding/agent_health_card.png new file mode 100644 index 0000000..23508e5 Binary files /dev/null and b/health_app/assets/branding/agent_health_card.png differ diff --git a/health_app/assets/branding/agent_medication_card.png b/health_app/assets/branding/agent_medication_card.png new file mode 100644 index 0000000..64e9b94 Binary files /dev/null and b/health_app/assets/branding/agent_medication_card.png differ diff --git a/health_app/assets/branding/agent_report_card.png b/health_app/assets/branding/agent_report_card.png new file mode 100644 index 0000000..9430fe7 Binary files /dev/null and b/health_app/assets/branding/agent_report_card.png differ diff --git a/health_app/lib/core/api_client.dart b/health_app/lib/core/api_client.dart index 60851d9..598bde9 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://localhost:5000'; +const String baseUrl = 'http://localhost:5000'; // adb reverse 或同WiFi直连时改为PC的IP /// Dio HTTP 客户端封装——带 token 注入、401 自动刷新 class ApiClient { diff --git a/health_app/lib/core/app_colors.dart b/health_app/lib/core/app_colors.dart index 1784f46..09d7c7e 100644 --- a/health_app/lib/core/app_colors.dart +++ b/health_app/lib/core/app_colors.dart @@ -10,14 +10,23 @@ class AppColors { static const Color primaryDark = Color(0xFF5B3FE8); static const Color blueMeasure = Color(0xFF4F7CFF); static const Color doctorBlue = Color(0xFF3B82F6); + static const Color auraLavender = Color(0xFFA78BFA); + static const Color auraIndigo = Color(0xFF818CF8); + static const Color auraOrchid = Color(0xFFC084FC); + static const Color auraDeepIndigo = Color(0xFF6366F1); + static const Color mintAccent = Color(0xFF2DD4BF); + static const Color peachAccent = Color(0xFFFFA6A6); + static const Color roseAccent = Color(0xFFF43F5E); + static const Color meadowAccent = Color(0xFF72F874); + static const Color sageAccent = Color(0xFF9CD9C9); - static const Color background = Color(0xFFF7F8FC); - static const Color backgroundSoft = Color(0xFFFBFCFF); + static const Color background = Color(0xFFFAF8FF); + static const Color backgroundSoft = Color(0xFFFFFCFF); static const Color cardBackground = Color(0xFFFFFFFF); - static const Color cardInner = Color(0xFFF8FAFF); + static const Color cardInner = Color(0xFFF7F5FF); static const Color pageGrey = background; - static const Color iconBg = Color(0xFFF0F4FF); - static const Color avatarBg = Color(0xFFF3F0FF); + static const Color iconBg = Color(0xFFF2EEFF); + static const Color avatarBg = Color(0xFFF4EFFF); static const Color textPrimary = Color(0xFF1F2937); static const Color textSecondary = Color(0xFF667085); @@ -39,20 +48,76 @@ class AppColors { static const LinearGradient primaryGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF7C5CFF), Color(0xFF4F7CFF)], + colors: [Color(0xFF8B5CF6), Color(0xFF7C5CFF), Color(0xFF6366F1)], + stops: [0.0, 0.5, 1.0], ); static const LinearGradient bgGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFFFFFFF), Color(0xFFF7F8FC), Color(0xFFF3F0FF)], - stops: [0.0, 0.68, 1.0], + colors: [ + Color(0xFFFFFCFF), + Color(0xFFF9F5FF), + Color(0xFFF5F1FF), + Color(0xFFF2F6FF), + ], + stops: [0.0, 0.42, 0.74, 1.0], ); static const LinearGradient drawerGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFFFFFFF), Color(0xFFF6F3FF)], + colors: [Color(0xFFFFFFFF), Color(0xFFFBF7FF), Color(0xFFF0F4FF)], + stops: [0.0, 0.52, 1.0], + ); + + static const LinearGradient surfaceGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFFFFFF), Color(0xFFFCFAFF)], + ); + + static const LinearGradient softGlassGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xF7FFFFFF), Color(0xECFFFFFF)], + ); + + static const LinearGradient roseVioletGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF8B5CF6), Color(0xFFF43F5E)], + ); + + static const LinearGradient sageVioletGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF9CD9C9), Color(0xFFAE2EF9)], + ); + + static const LinearGradient meadowVioletGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF72F874), Color(0xFF8C6BD9)], + ); + + static const LinearGradient calmHealthGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF9CD9C9), Color(0xFF818CF8)], + ); + + static const LinearGradient warmCareGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFFA6A6), Color(0xFFC084FC)], + ); + + static const LinearGradient actionOutlineGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFC084FC), Color(0xFF818CF8), Color(0xFFC084FC), Color(0xFF818CF8)], + stops: [0.0, 0.3, 0.6, 1.0], ); static const LinearGradient doctorGradient = LinearGradient( diff --git a/health_app/lib/core/app_theme.dart b/health_app/lib/core/app_theme.dart index dbef414..750a525 100644 --- a/health_app/lib/core/app_theme.dart +++ b/health_app/lib/core/app_theme.dart @@ -2,6 +2,23 @@ import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'app_colors.dart'; +class AppBackground extends StatelessWidget { + final Widget child; + final bool safeArea; + + const AppBackground({super.key, required this.child, this.safeArea = false}); + + @override + Widget build(BuildContext context) { + final content = safeArea ? SafeArea(child: child) : child; + + return DecoratedBox( + decoration: const BoxDecoration(gradient: AppColors.bgGradient), + child: content, + ); + } +} + class GradientScaffold extends StatelessWidget { final PreferredSizeWidget? appBar; final Widget? body; @@ -21,8 +38,7 @@ class GradientScaffold extends StatelessWidget { }); @override - Widget build(BuildContext context) => Container( - decoration: const BoxDecoration(gradient: AppColors.bgGradient), + Widget build(BuildContext context) => AppBackground( child: Scaffold( backgroundColor: Colors.transparent, appBar: appBar, @@ -123,34 +139,34 @@ class AppTheme { surface: surface, brightness: Brightness.light, ), - scaffoldBackgroundColor: bg, + scaffoldBackgroundColor: Colors.transparent, splashColor: primary.withValues(alpha: 0.06), highlightColor: Colors.transparent, hoverColor: primaryLight.withValues(alpha: 0.55), fontFamily: null, - appBarTheme: const AppBarTheme( - backgroundColor: surface, + appBarTheme: AppBarTheme( + backgroundColor: Colors.white.withValues(alpha: 0.9), foregroundColor: text, elevation: 0, centerTitle: true, scrolledUnderElevation: 0, surfaceTintColor: Colors.transparent, - titleTextStyle: TextStyle( + titleTextStyle: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: text, ), - toolbarHeight: 48, + toolbarHeight: 52, ), cardTheme: CardThemeData( - color: surface, + color: Colors.white, elevation: 0, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(rLg), - side: const BorderSide(color: border), + side: const BorderSide(color: AppColors.borderLight), ), margin: EdgeInsets.zero, ), @@ -161,15 +177,15 @@ class AppTheme { contentPadding: const EdgeInsets.symmetric(horizontal: sLg, vertical: 13), border: OutlineInputBorder( borderRadius: BorderRadius.circular(rMd), - borderSide: const BorderSide(color: border), + borderSide: const BorderSide(color: AppColors.borderLight), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rMd), - borderSide: const BorderSide(color: border), + borderSide: const BorderSide(color: AppColors.borderLight), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rMd), - borderSide: const BorderSide(color: primary, width: 1.2), + borderSide: const BorderSide(color: AppColors.auraIndigo, width: 1.3), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(rMd), @@ -183,7 +199,7 @@ class AppTheme { backgroundColor: primary, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rMd)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(rLg)), textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), elevation: 0, ), diff --git a/health_app/lib/pages/auth/login_page.dart b/health_app/lib/pages/auth/login_page.dart index b5c4b6c..a0578a5 100644 --- a/health_app/lib/pages/auth/login_page.dart +++ b/health_app/lib/pages/auth/login_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/app_colors.dart'; +import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/data_providers.dart'; @@ -243,106 +244,104 @@ class _LoginPageState extends ConsumerState { } return Scaffold( - body: Container( - decoration: const BoxDecoration(gradient: AppColors.bgGradient), - child: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), - child: Container( - padding: const EdgeInsets.fromLTRB(22, 24, 22, 22), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadow, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _BrandMark(), - const SizedBox(height: 16), - const Text( - '健康管家', - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.w900, - color: AppColors.textPrimary, - ), + body: AppBackground( + safeArea: true, + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: Container( + padding: const EdgeInsets.fromLTRB(22, 24, 22, 22), + decoration: BoxDecoration( + gradient: AppColors.softGlassGradient, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadow, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _BrandMark(), + const SizedBox(height: 16), + const Text( + '健康管家', + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, ), - const SizedBox(height: 6), - Text( - _isLogin ? '登录你的健康账户' : '创建新的健康账户', + ), + const SizedBox(height: 6), + Text( + _isLogin ? '登录你的健康账户' : '创建新的健康账户', + style: const TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 26), + if (_successMsg != null) + _NoticeBanner(text: _successMsg!, success: true), + if (!_isLogin) ...[ + _DoctorSelector( + name: _selectedDoctorName, + onTap: _showDoctorPicker, + ), + const SizedBox(height: 12), + _TextField(_nameCtrl, '姓名', TextInputType.name, 20), + const SizedBox(height: 12), + ], + _TextField(_phoneCtrl, '手机号', TextInputType.phone, 11), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _TextField( + _codeCtrl, + '验证码', + TextInputType.number, + 6, + ), + ), + const SizedBox(width: 10), + _SmsButton( + sending: _sending, + countdown: _countdown, + onTap: (_countdown > 0 || _sending) ? null : _sendSms, + ), + ], + ), + const SizedBox(height: 14), + _Agreement( + agreed: _agreed, + onTap: () => setState(() => _agreed = !_agreed), + ), + if (_error != null) ...[ + const SizedBox(height: 12), + _NoticeBanner(text: _error!, success: false), + ], + const SizedBox(height: 22), + _PrimaryButton( + loading: _loading, + label: _isLogin ? '登录' : '注册', + onTap: _loading ? null : _submit, + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () => setState(() { + _isLogin = !_isLogin; + _error = null; + _successMsg = null; + }), + child: Text( + _isLogin ? '没有账号?去注册' : '已有账号?去登录', style: const TextStyle( fontSize: 14, - color: AppColors.textSecondary, + fontWeight: FontWeight.w700, + color: AppColors.primary, ), ), - const SizedBox(height: 26), - if (_successMsg != null) - _NoticeBanner(text: _successMsg!, success: true), - if (!_isLogin) ...[ - _DoctorSelector( - name: _selectedDoctorName, - onTap: _showDoctorPicker, - ), - const SizedBox(height: 12), - _TextField(_nameCtrl, '姓名', TextInputType.name, 20), - const SizedBox(height: 12), - ], - _TextField(_phoneCtrl, '手机号', TextInputType.phone, 11), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _TextField( - _codeCtrl, - '验证码', - TextInputType.number, - 6, - ), - ), - const SizedBox(width: 10), - _SmsButton( - sending: _sending, - countdown: _countdown, - onTap: (_countdown > 0 || _sending) ? null : _sendSms, - ), - ], - ), - const SizedBox(height: 14), - _Agreement( - agreed: _agreed, - onTap: () => setState(() => _agreed = !_agreed), - ), - if (_error != null) ...[ - const SizedBox(height: 12), - _NoticeBanner(text: _error!, success: false), - ], - const SizedBox(height: 22), - _PrimaryButton( - loading: _loading, - label: _isLogin ? '登录' : '注册', - onTap: _loading ? null : _submit, - ), - const SizedBox(height: 16), - GestureDetector( - onTap: () => setState(() { - _isLogin = !_isLogin; - _error = null; - _successMsg = null; - }), - child: Text( - _isLogin ? '没有账号?去注册' : '已有账号?去登录', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.primary, - ), - ), - ), - ], - ), + ), + ], ), ), ), diff --git a/health_app/lib/pages/device/device_management_page.dart b/health_app/lib/pages/device/device_management_page.dart index 0e8ff88..15cd8da 100644 --- a/health_app/lib/pages/device/device_management_page.dart +++ b/health_app/lib/pages/device/device_management_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/app_colors.dart'; +import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/omron_device_provider.dart'; @@ -88,8 +89,7 @@ class _DeviceManagementPageState extends ConsumerState { Widget build(BuildContext context) { final device = ref.watch(omronDeviceProvider); - return Scaffold( - backgroundColor: AppColors.background, + return GradientScaffold( appBar: AppBar( title: const Text('蓝牙设备'), leading: IconButton( @@ -118,14 +118,15 @@ class _DeviceManagementPageState extends ConsumerState { width: 82, height: 82, decoration: BoxDecoration( - color: AppColors.iconBg, + gradient: AppColors.calmHealthGradient, borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadowLight, ), child: const Icon( Icons.bluetooth_disabled, size: 40, - color: AppColors.primary, + color: Colors.white, ), ), const SizedBox(height: 16), @@ -156,10 +157,10 @@ class _DeviceManagementPageState extends ConsumerState { duration: const Duration(milliseconds: 250), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(18), border: Border.all( - color: d.isConnected ? AppColors.success : AppColors.border, + color: d.isConnected ? AppColors.success : AppColors.borderLight, ), boxShadow: AppColors.cardShadowLight, ), @@ -169,15 +170,16 @@ class _DeviceManagementPageState extends ConsumerState { width: 50, height: 50, decoration: BoxDecoration( - color: d.isConnected - ? AppColors.successLight - : AppColors.cardInner, + gradient: d.isConnected + ? AppColors.calmHealthGradient + : AppColors.surfaceGradient, borderRadius: BorderRadius.circular(15), + border: Border.all(color: AppColors.borderLight), ), child: Icon( Icons.bluetooth, size: 24, - color: d.isConnected ? AppColors.success : AppColors.textHint, + color: d.isConnected ? Colors.white : AppColors.textHint, ), ), const SizedBox(width: 14), @@ -250,9 +252,9 @@ class _DeviceManagementPageState extends ConsumerState { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(18), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -334,9 +336,9 @@ class _DeviceManagementPageState extends ConsumerState { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(18), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Column( diff --git a/health_app/lib/pages/device/device_scan_page.dart b/health_app/lib/pages/device/device_scan_page.dart index 4cb83f8..38c5f91 100644 --- a/health_app/lib/pages/device/device_scan_page.dart +++ b/health_app/lib/pages/device/device_scan_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../core/app_colors.dart'; +import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/omron_device_provider.dart'; @@ -183,10 +184,9 @@ class _DeviceScanPageState extends ConsumerState @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, + return GradientScaffold( appBar: AppBar( - backgroundColor: Colors.white, + backgroundColor: Colors.white.withValues(alpha: 0.9), elevation: 0, title: Text( _connected ? _deviceName : '添加设备', @@ -256,7 +256,7 @@ class _DeviceScanPageState extends ConsumerState height: 80 * _pulseAnim.value, decoration: BoxDecoration( shape: BoxShape.circle, - color: AppColors.primary.withValues(alpha: 0.08), + color: AppColors.auraIndigo.withValues(alpha: 0.10), ), ), ), @@ -264,13 +264,14 @@ class _DeviceScanPageState extends ConsumerState width: 64, height: 64, decoration: BoxDecoration( - color: AppColors.avatarBg, + gradient: AppColors.calmHealthGradient, borderRadius: BorderRadius.circular(20), + boxShadow: AppColors.buttonShadow, ), child: const Icon( Icons.bluetooth_searching, size: 32, - color: AppColors.primary, + color: Colors.white, ), ), ], @@ -301,19 +302,18 @@ class _DeviceScanPageState extends ConsumerState width: 100, height: 100, decoration: BoxDecoration( - color: _readingReceived - ? AppColors.successLight - : AppColors.avatarBg, + gradient: _readingReceived + ? AppColors.calmHealthGradient + : AppColors.primaryGradient, borderRadius: BorderRadius.circular(32), + boxShadow: AppColors.buttonShadow, ), child: Icon( _readingReceived ? Icons.check_rounded : Icons.bluetooth_connected, size: 48, - color: _readingReceived - ? const Color(0xFF10B981) - : AppColors.primary, + color: _readingReceived ? Colors.white : Colors.white, ), ), const SizedBox(height: 24), @@ -456,9 +456,9 @@ class _DeviceScanPageState extends ConsumerState margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -467,14 +467,10 @@ class _DeviceScanPageState extends ConsumerState width: 44, height: 44, decoration: BoxDecoration( - color: AppColors.avatarBg, + gradient: AppColors.calmHealthGradient, borderRadius: BorderRadius.circular(12), ), - child: const Icon( - Icons.bluetooth, - size: 22, - color: AppColors.primary, - ), + child: const Icon(Icons.bluetooth, size: 22, color: Colors.white), ), const SizedBox(width: 12), Expanded( @@ -515,8 +511,9 @@ class _DeviceScanPageState extends ConsumerState vertical: 10, ), decoration: BoxDecoration( - color: AppColors.primary, + gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(12), + boxShadow: AppColors.buttonShadow, ), child: const Text( '连接', diff --git a/health_app/lib/pages/diet/diet_capture_page.dart b/health_app/lib/pages/diet/diet_capture_page.dart index 5854579..cd7ddfb 100644 --- a/health_app/lib/pages/diet/diet_capture_page.dart +++ b/health_app/lib/pages/diet/diet_capture_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/navigation_provider.dart'; import '../../core/app_colors.dart'; +import '../../core/app_theme.dart'; import '../../providers/auth_provider.dart'; import '../../utils/sse_handler.dart'; @@ -303,7 +304,6 @@ class DietNotifier extends Notifier { // ─────────── 饮食主题色(暖橙系,不再用紫色)─────────── const _dietAccent = AppColors.primary; const _dietAccentLight = AppColors.primarySoft; -const _dietBg = AppColors.background; class DietCapturePage extends ConsumerStatefulWidget { const DietCapturePage({super.key}); @@ -320,10 +320,9 @@ class _DietCapturePageState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(dietProvider); - return Scaffold( - backgroundColor: _dietBg, + return GradientScaffold( appBar: AppBar( - backgroundColor: Colors.white, + backgroundColor: Colors.white.withValues(alpha: 0.9), title: const Text( '饮食分析', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), @@ -352,13 +351,22 @@ class _DietCapturePageState extends ConsumerState { child: Column( children: [ // 图片自适应显示 - ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Image.file( - File(state.imagePath!), - width: screenW - 32, - height: 220, - fit: BoxFit.cover, + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadowLight, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.file( + File(state.imagePath!), + width: screenW - 48, + height: 220, + fit: BoxFit.cover, + ), ), ), const SizedBox(height: 16), @@ -398,9 +406,9 @@ class _DietCapturePageState extends ConsumerState { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -413,7 +421,8 @@ class _DietCapturePageState extends ConsumerState { duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: sel ? _dietAccentLight : Colors.white, + gradient: sel ? AppColors.warmCareGradient : null, + color: sel ? null : Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( @@ -426,7 +435,7 @@ class _DietCapturePageState extends ConsumerState { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: sel ? _dietAccent : AppColors.textHint, + color: sel ? Colors.white : AppColors.textHint, ), ), ], @@ -876,28 +885,40 @@ class _DietCapturePageState extends ConsumerState { // ─────────── 保存按钮 ─────────── Widget _buildSaveButton() { - return SizedBox( + return Container( width: double.infinity, height: 52, - child: ElevatedButton.icon( - onPressed: () async { - try { - await ref.read(dietProvider.notifier).saveRecord(); - if (mounted) popRoute(ref); - } catch (e) { - debugPrint('[Diet] 保存记录失败: $e'); - } - }, - icon: const Icon(Icons.check_circle_outline, size: 22), - label: const Text( - '保存记录', - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), - ), - style: ElevatedButton.styleFrom( - backgroundColor: _dietAccent, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + decoration: BoxDecoration( + gradient: AppColors.warmCareGradient, + borderRadius: BorderRadius.circular(16), + boxShadow: AppColors.buttonShadow, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + try { + await ref.read(dietProvider.notifier).saveRecord(); + if (mounted) popRoute(ref); + } catch (e) { + debugPrint('[Diet] 保存记录失败: $e'); + } + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.check_circle_outline, size: 22, color: Colors.white), + SizedBox(width: 8), + Text( + '保存记录', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w800, + color: Colors.white, + ), + ), + ], ), ), ), diff --git a/health_app/lib/pages/home/home_page.dart b/health_app/lib/pages/home/home_page.dart index 7b1d0fd..29db7fd 100644 --- a/health_app/lib/pages/home/home_page.dart +++ b/health_app/lib/pages/home/home_page.dart @@ -5,6 +5,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../core/app_colors.dart'; +import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/chat_provider.dart'; @@ -85,21 +86,19 @@ class _HomePageState extends ConsumerState { return Scaffold( drawer: const HealthDrawer(), - body: Container( - decoration: const BoxDecoration(gradient: AppColors.bgGradient), - child: SafeArea( - child: Column( - children: [ - _buildHeader(user), - Expanded( - child: ChatMessagesView( - scrollCtrl: _scrollCtrl, - messages: chatState.messages, - ), + body: AppBackground( + safeArea: true, + child: Column( + children: [ + _buildHeader(user), + Expanded( + child: ChatMessagesView( + scrollCtrl: _scrollCtrl, + messages: chatState.messages, ), - _buildBottomBar(context), - ], - ), + ), + _buildBottomBar(context), + ], ), ), ); @@ -110,10 +109,19 @@ class _HomePageState extends ConsumerState { ? user.name : '用户'; return Container( - padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), + padding: const EdgeInsets.fromLTRB(14, 10, 14, 10), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.72), - border: const Border(bottom: BorderSide(color: AppColors.divider)), + color: Colors.white.withValues(alpha: 0.34), + border: Border( + bottom: BorderSide(color: Colors.white.withValues(alpha: 0.62)), + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF101828).withValues(alpha: 0.025), + blurRadius: 16, + offset: const Offset(0, 6), + ), + ], ), child: Row( children: [ @@ -178,7 +186,7 @@ class _HomePageState extends ConsumerState { Widget _buildAgentBar() { return SizedBox( - height: 42, + height: 46, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 14), @@ -186,21 +194,36 @@ class _HomePageState extends ConsumerState { separatorBuilder: (_, _) => const SizedBox(width: 8), itemBuilder: (_, i) { final (agent, label, icon) = _agentDefs[i]; + final gradient = _agentGradient(agent); return GestureDetector( onTap: () => ref.read(chatProvider.notifier).triggerAgent(agent, label), child: Container( padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 9), decoration: BoxDecoration( - color: Colors.white, + color: Colors.white.withValues(alpha: 0.86), borderRadius: BorderRadius.circular(999), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, + border: Border.all(color: Colors.white, width: 1.2), + boxShadow: [ + BoxShadow( + color: const Color(0xFF6366F1).withValues(alpha: 0.10), + blurRadius: 14, + offset: const Offset(0, 7), + ), + ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 16, color: AppColors.primary), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(6), + ), + child: Icon(icon, size: 11, color: Colors.white), + ), const SizedBox(width: 6), Text( label, @@ -219,17 +242,39 @@ class _HomePageState extends ConsumerState { ); } + LinearGradient _agentGradient(ActiveAgent agent) { + return switch (agent) { + ActiveAgent.consultation => AppColors.sageVioletGradient, + ActiveAgent.health => AppColors.meadowVioletGradient, + ActiveAgent.diet => AppColors.warmCareGradient, + ActiveAgent.medication => AppColors.primaryGradient, + ActiveAgent.report => AppColors.roseVioletGradient, + ActiveAgent.exercise => AppColors.calmHealthGradient, + _ => AppColors.primaryGradient, + }; + } + Widget _buildBottomBar(BuildContext context) { return Container( decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.76), - border: const Border(top: BorderSide(color: AppColors.divider)), + color: Colors.white.withValues(alpha: 0.28), + border: Border( + top: BorderSide(color: Colors.white.withValues(alpha: 0.62)), + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF101828).withValues(alpha: 0.045), + blurRadius: 20, + offset: const Offset(0, -8), + ), + ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), + const SizedBox(height: 10), _buildAgentBar(), + const SizedBox(height: 8), if (_pickedImagePath != null) _buildImagePreview(), _buildInputBar(), ], @@ -242,9 +287,9 @@ class _HomePageState extends ConsumerState { margin: const EdgeInsets.fromLTRB(14, 8, 14, 0), padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: AppColors.cardInner, + color: Colors.white.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), ), child: Row( children: [ @@ -275,7 +320,7 @@ class _HomePageState extends ConsumerState { Widget _buildInputBar() { return Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -306,15 +351,18 @@ class _HomePageState extends ConsumerState { ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(22), - borderSide: const BorderSide(color: AppColors.border), + borderSide: const BorderSide(color: AppColors.borderLight), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(22), - borderSide: const BorderSide(color: AppColors.border), + borderSide: const BorderSide(color: AppColors.borderLight), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(22), - borderSide: const BorderSide(color: AppColors.primary), + borderSide: const BorderSide( + color: AppColors.auraIndigo, + width: 1.2, + ), ), ), onSubmitted: (_) => _sendMessage(), @@ -442,9 +490,10 @@ class _HeaderIconButton extends StatelessWidget { width: 38, height: 38, decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadowLight, ), child: Icon(icon, size: 20, color: AppColors.textPrimary), ), @@ -466,11 +515,12 @@ class _RoundToolButton extends StatelessWidget { height: 42, margin: const EdgeInsets.only(bottom: 2), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(21), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadowLight, ), - child: Icon(icon, size: 21, color: AppColors.textSecondary), + child: Icon(icon, size: 21, color: AppColors.primaryDark), ), ); } 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 e6d7a15..a2162e4 100644 --- a/health_app/lib/pages/home/widgets/chat_messages_view.dart +++ b/health_app/lib/pages/home/widgets/chat_messages_view.dart @@ -133,7 +133,9 @@ class ChatMessagesView extends ConsumerWidget { case MessageType.dataConfirm: return _buildDataConfirmCard(context, ref, msg); default: - if (!msg.isUser && chatState.isStreaming && msg.content.length < 5) { + if (!msg.isUser && + chatState.isStreaming && + msg.content.trim().isEmpty) { return _buildThinkingBubble(context, chatState.thinkingText); } return _buildTextBubble(context, msg, chatState); @@ -152,14 +154,9 @@ class ChatMessagesView extends ConsumerWidget { ) { final info = _agentInfo(agent); 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, - ); + final artworkPath = _agentArtworkPath(agent); return _AnimatedCardEntry( child: Align( @@ -168,10 +165,20 @@ class ChatMessagesView extends ConsumerWidget { margin: const EdgeInsets.only(bottom: 16), constraints: BoxConstraints(maxWidth: screenWidth * 0.95), decoration: BoxDecoration( - color: AppColors.cardBackground, + color: Colors.white, borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadow, + boxShadow: [ + BoxShadow( + color: const Color(0xFF6B5CE7).withValues(alpha: 0.12), + blurRadius: 28, + offset: const Offset(0, 14), + ), + BoxShadow( + color: const Color(0xFF101828).withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), clipBehavior: Clip.antiAlias, child: Column( @@ -180,40 +187,31 @@ class ChatMessagesView extends ConsumerWidget { // ── 标题区域 ── Stack( children: [ - Positioned.fill( - child: Opacity( - opacity: 0.72, - child: Image.asset( - 'assets/branding/agent_welcome_abstract.png', - fit: BoxFit.cover, - ), - ), - ), Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.white.withValues(alpha: 0.95), - agentColors.bg.withValues(alpha: 0.70), - Colors.white.withValues(alpha: 0.28), + Colors.white, + agentColors.bg.withValues(alpha: 0.86), + agentColors.accent.withValues(alpha: 0.12), ], - stops: const [0.0, 0.46, 1.0], - begin: Alignment.centerLeft, - end: Alignment.centerRight, + stops: const [0.0, 0.58, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), ), ), ), Positioned( - right: -18, - top: -22, - child: _SignalHalo(colors: agentColors), + right: 18, + top: 18, + child: _MiniSignalStrip(colors: agentColors), ), Positioned( right: 18, - bottom: 14, - child: _MiniSignalStrip(colors: agentColors), + bottom: 18, + child: _LinearAccentMark(colors: agentColors), ), Container( width: double.infinity, @@ -288,119 +286,13 @@ class ChatMessagesView extends ConsumerWidget { ], ), - // ── 功能特性区 ── - if (features.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 0), - child: Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(18), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 4, - height: 16, - decoration: BoxDecoration( - gradient: iconGradient, - 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: const EdgeInsets.symmetric( - horizontal: 3, - ), - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 6, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: agentColors.accent.withValues( - alpha: 0.16, - ), - ), - boxShadow: [ - BoxShadow( - color: agentColors.accent.withValues( - alpha: 0.05, - ), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: agentColors.iconBg, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: agentColors.accent.withValues( - alpha: 0.12, - ), - ), - ), - 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, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - ], - ), - ), - ), - ], + // ── 插画展示 ── + Image.asset( + artworkPath, + width: double.infinity, + height: 206, + fit: BoxFit.cover, + ), // ── 医生选择区(问诊专用)── if (agent == ActiveAgent.consultation) ...[ @@ -411,51 +303,24 @@ class ChatMessagesView extends ConsumerWidget { ), ], - // ── 快捷操作按钮区 ── if (actions.isNotEmpty) ...[ const SizedBox(height: 14), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - children: [ - Container( - width: 6, - height: 18, - decoration: BoxDecoration( - gradient: iconGradient, - borderRadius: BorderRadius.circular(3), - ), + for (int i = 0; i < actions.length; i++) ...[ + if (i > 0) const SizedBox(width: 10), + Flexible( + child: _buildActionButton( + actions[i], + context, + ref, + agentColors, ), - const SizedBox(width: 8), - Text( - '快捷操作', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - ), - ), - ], - ), - const SizedBox(height: 12), - 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, - ), - ), - ], - ], - ), + ), + ], ], ), ), @@ -527,81 +392,44 @@ class ChatMessagesView extends ConsumerWidget { } } }, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(28), child: Container( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + padding: const EdgeInsets.all(1.4), decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.white, colors.bg.withValues(alpha: 0.82)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: colors.accent.withValues(alpha: 0.18)), - boxShadow: AppColors.cardShadowLight, + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(28), ), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: colors.iconBg, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: colors.accent.withValues(alpha: 0.14), - ), - ), - child: Icon(a.icon, size: 22, color: colors.accent), - ), - const SizedBox(width: 12), - Expanded( - child: Text( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.5), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(a.icon, size: 24, color: AppColors.textPrimary), + const SizedBox(width: 10), + Text( a.label, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary), ), - ), - Icon(Icons.chevron_right, size: 18, color: AppColors.border), - ], + ], + ), ), ), ); } - List<_AgentFeature> _getAgentFeatures(ActiveAgent agent) { + String _agentArtworkPath(ActiveAgent agent) { return switch (agent) { - ActiveAgent.health => [ - _AgentFeature(Icons.trending_up, '趋势分析', '查看历史数据'), - _AgentFeature(Icons.add, '快速录入', '血压心率血糖'), - ], - ActiveAgent.diet => [ - _AgentFeature(Icons.camera_alt, '拍照识别', '自动识别食物'), - _AgentFeature(Icons.bar_chart, '营养分析', '热量营养成分'), - ], - ActiveAgent.medication => [ - _AgentFeature(Icons.list, '用药清单', '管理所有药品'), - _AgentFeature(Icons.alarm, '智能提醒', '按时服药提醒'), - ], - ActiveAgent.consultation => [ - _AgentFeature(Icons.message, '在线问诊', '专业医生解答'), - _AgentFeature(Icons.local_hospital, '科室选择', '精准匹配'), - ], - ActiveAgent.report => [ - _AgentFeature(Icons.upload, '上传报告', '支持多种格式'), - _AgentFeature(Icons.search, 'AI解读', '智能分析报告'), - ], - ActiveAgent.exercise => [ - _AgentFeature(Icons.calendar_month, '运动计划', '科学规划'), - _AgentFeature(Icons.check_circle, '打卡记录', '坚持完成'), - ], - _ => [ - _AgentFeature(Icons.health_and_safety, '健康管理', '全方位呵护'), - _AgentFeature(Icons.chat, '智能咨询', '随时答疑'), - ], + ActiveAgent.consultation => 'assets/branding/agent_consultation_card.png', + ActiveAgent.health => 'assets/branding/agent_health_card.png', + ActiveAgent.diet => 'assets/branding/agent_diet_card.png', + ActiveAgent.medication => 'assets/branding/agent_medication_card.png', + ActiveAgent.report => 'assets/branding/agent_report_card.png', + ActiveAgent.exercise => 'assets/branding/agent_exercise_card.png', + _ => 'assets/branding/agent_health_card.png', }; } @@ -855,10 +683,20 @@ class ChatMessagesView extends ConsumerWidget { margin: const EdgeInsets.only(bottom: 16), constraints: BoxConstraints(maxWidth: screenWidth * 0.95), decoration: BoxDecoration( - color: AppColors.cardBackground, + color: Colors.white, borderRadius: BorderRadius.circular(28), - border: Border.all(color: Color(0xFFE2E8F0), width: 2), - boxShadow: AppColors.cardShadow, + boxShadow: [ + BoxShadow( + color: const Color(0xFF6366F1).withValues(alpha: 0.10), + blurRadius: 28, + offset: const Offset(0, 14), + ), + BoxShadow( + color: const Color(0xFF101828).withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), clipBehavior: Clip.antiAlias, child: Column( @@ -868,7 +706,13 @@ class ChatMessagesView extends ConsumerWidget { Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), - decoration: const BoxDecoration(color: Color(0xFFF8F6FF)), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFFFFFF), Color(0xFFF7F4FF)], + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -876,11 +720,11 @@ class ChatMessagesView extends ConsumerWidget { width: 56, height: 56, decoration: BoxDecoration( - color: AppColors.cardInner, + gradient: AppColors.actionOutlineGradient, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: AppColors.primaryPurple.withAlpha(35), + color: AppColors.auraIndigo.withValues(alpha: 0.18), blurRadius: 10, offset: const Offset(0, 4), ), @@ -930,85 +774,92 @@ class ChatMessagesView extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 0), child: Container( - width: double.infinity, - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(1.4), decoration: BoxDecoration( - color: AppColors.backgroundSecondary, - borderRadius: BorderRadius.circular(18), - border: Border.all(color: AppColors.border), + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(20), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 68, - height: 68, - decoration: BoxDecoration( - gradient: AppColors.lightGradient, - borderRadius: BorderRadius.circular(18), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 68, + height: 68, + decoration: BoxDecoration( + gradient: AppColors.lightGradient, + borderRadius: BorderRadius.circular(18), + ), + child: Center( + child: isHealth + ? Icon( + _getMetricIconData(backendType), + size: 34, + color: AppColors.primaryDark, + ) + : Icon( + isMedication + ? Icons.medication_liquid_outlined + : Icons.directions_run_outlined, + size: 36, + color: AppColors.iconColor, + ), + ), ), - child: Center( - child: isHealth - ? Text( - _getMetricIcon(backendType), - style: const TextStyle(fontSize: 36), - ) - : Icon( - isMedication - ? Icons.medication_liquid_outlined - : Icons.directions_run_outlined, - size: 36, - color: AppColors.iconColor, + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + mainLabel, + style: const TextStyle( + fontSize: 17, + color: AppColors.textSecondary, ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - mainLabel, - style: const TextStyle( - fontSize: 17, - color: AppColors.textSecondary, ), - ), - if (mainValue.isNotEmpty) ...[ - const SizedBox(height: 8), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: 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, + fontSize: 36, + fontWeight: FontWeight.w800, color: abnormal ? AppColors.error - : AppColors.iconColor, - fontWeight: FontWeight.w500, + : AppColors.textPrimary, + letterSpacing: -0.5, ), ), - ], + if (mainUnit.isNotEmpty) + TextSpan( + text: ' $mainUnit', + style: TextStyle( + fontSize: 19, + color: abnormal + ? AppColors.error + : AppColors.iconColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), - ), + ], ], - ], + ), ), - ), - ], + ], + ), ), ), ), @@ -1098,36 +949,38 @@ class ChatMessagesView extends ConsumerWidget { child: Container( width: double.infinity, height: 56, + padding: const EdgeInsets.all(1.4), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.actionOutlineGradient, borderRadius: BorderRadius.circular(20), - border: Border.all( - color: AppColors.borderLight, - width: 1.5, - ), - boxShadow: AppColors.cardShadow, + boxShadow: [ + BoxShadow( + color: AppColors.auraIndigo.withValues(alpha: 0.18), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], ), child: msg.confirmed ? Container( decoration: BoxDecoration( gradient: AppColors.successButtonGradient, - borderRadius: BorderRadius.circular(20), - boxShadow: AppColors.buttonShadow, + borderRadius: BorderRadius.circular(18), ), - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.check_circle_outline, size: 22, color: Colors.white, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( '录入成功', style: TextStyle( fontSize: 19, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, color: Colors.white, ), ), @@ -1142,33 +995,39 @@ class ChatMessagesView extends ConsumerWidget { .read(chatProvider.notifier) .confirmMessage(msg.id); }, - borderRadius: BorderRadius.circular(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - gradient: AppColors.purpleBlueGradient, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(18), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.check_rounded, + size: 19, + color: Colors.white, + ), ), - child: const Icon( - Icons.check, - size: 18, - color: Colors.white, + const SizedBox(width: 10), + const Text( + '确认录入', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), ), - ), - const SizedBox(width: 10), - Text( - '确认录入', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - ), - ], + ], + ), ), ), ), @@ -1249,7 +1108,9 @@ class ChatMessagesView extends ConsumerWidget { key: ValueKey('editing_${msg.id}_${field.label}'), initialValue: field.value, onDone: (value) { - ref.read(chatProvider.notifier).finishEditingField(msg.id, field.label, value); + ref + .read(chatProvider.notifier) + .finishEditingField(msg.id, field.label, value); }, ); } @@ -1612,22 +1473,22 @@ class ChatMessagesView extends ConsumerWidget { } } - String _getMetricIcon(String type) { + IconData _getMetricIconData(String type) { switch (type.toLowerCase()) { case 'blood_pressure': - return '🩺'; + return Icons.bloodtype_outlined; case 'heart_rate': - return '💓'; + return Icons.monitor_heart_outlined; case 'glucose': - return '💉'; + return Icons.water_drop_outlined; case 'spo2': - return '🫁'; + return Icons.air_outlined; case 'weight': - return '⚖️'; + return Icons.monitor_weight_outlined; case 'exercise': - return '🏃'; + return Icons.directions_run_outlined; default: - return '📊'; + return Icons.query_stats_outlined; } } @@ -1692,46 +1553,46 @@ class ChatMessagesView extends ConsumerWidget { static _AgentColors _agentColors(ActiveAgent agent) { return switch (agent) { ActiveAgent.consultation => _AgentColors( - gradient: [AppColors.primary, AppColors.blueMeasure], - bg: const Color(0xFFF3F6FF), - border: const Color(0xFFDCE5FF), - iconBg: const Color(0xFFEFF4FF), - accent: AppColors.blueMeasure, + gradient: [AppColors.sageAccent, AppColors.primary], + bg: const Color(0xFFF2FBF9), + border: const Color(0xFFD9F0EA), + iconBg: const Color(0xFFE8F8F4), + accent: const Color(0xFF4F9E90), ), ActiveAgent.health => _AgentColors( - gradient: [AppColors.primary, const Color(0xFF26B99A)], - bg: const Color(0xFFF1FBF8), - border: const Color(0xFFD8F1EA), - iconBg: const Color(0xFFE9F8F4), - accent: const Color(0xFF26B99A), + gradient: [AppColors.meadowAccent, AppColors.auraDeepIndigo], + bg: const Color(0xFFF3FBF4), + border: const Color(0xFFDFF2E4), + iconBg: const Color(0xFFEFF9F0), + accent: const Color(0xFF4EA65B), ), ActiveAgent.diet => _AgentColors( - gradient: [AppColors.primary, const Color(0xFFFFA14F)], + gradient: [AppColors.peachAccent, AppColors.auraOrchid], bg: const Color(0xFFFFF7F0), border: const Color(0xFFFFE8D4), iconBg: const Color(0xFFFFF0E4), - accent: const Color(0xFFFF8A3D), + accent: const Color(0xFFE26A64), ), ActiveAgent.medication => _AgentColors( - gradient: [AppColors.primary, AppColors.blueMeasure], + gradient: [AppColors.auraLavender, AppColors.blueMeasure], bg: const Color(0xFFF4F6FF), border: const Color(0xFFDDE6FF), iconBg: const Color(0xFFEFF3FF), accent: AppColors.primary, ), ActiveAgent.report => _AgentColors( - gradient: [AppColors.primary, const Color(0xFF9B7CFF)], + gradient: [AppColors.primary, AppColors.roseAccent], bg: const Color(0xFFF8F5FF), border: const Color(0xFFE9E0FF), iconBg: const Color(0xFFF2EDFF), accent: AppColors.primary, ), ActiveAgent.exercise => _AgentColors( - gradient: [AppColors.primary, const Color(0xFF21A6B7)], + gradient: [AppColors.sageAccent, AppColors.auraDeepIndigo], bg: const Color(0xFFF0FBFD), border: const Color(0xFFD6EEF2), iconBg: const Color(0xFFE8F7FA), - accent: const Color(0xFF21A6B7), + accent: const Color(0xFF2B8EA0), ), _ => _AgentColors( gradient: [AppColors.primary, AppColors.blueMeasure], @@ -2022,7 +1883,10 @@ class ChatMessagesView extends ConsumerWidget { Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 12, 16, 10), - decoration: const BoxDecoration(gradient: AppColors.drawerGradient), + decoration: const BoxDecoration( + gradient: AppColors.drawerGradient, + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), child: Row( children: [ Container( @@ -2197,23 +2061,21 @@ class _AgentMark extends StatelessWidget { } } -class _SignalHalo extends StatelessWidget { +class _LinearAccentMark extends StatelessWidget { final _AgentColors colors; - const _SignalHalo({required this.colors}); + const _LinearAccentMark({required this.colors}); @override Widget build(BuildContext context) { return Container( - width: 118, - height: 118, + width: 86, + height: 8, decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - colors.accent.withValues(alpha: 0.18), - colors.accent.withValues(alpha: 0.06), - Colors.transparent, - ], + borderRadius: BorderRadius.circular(999), + gradient: LinearGradient( + colors: colors.gradient + .map((color) => color.withValues(alpha: 0.34)) + .toList(), ), ), ); @@ -2279,14 +2141,6 @@ class _AgentAction { }); } -class _AgentFeature { - final IconData icon; - final String title; - final String desc; - - const _AgentFeature(this.icon, this.title, this.desc); -} - class _ConfirmField { final String label; final String value; @@ -2303,11 +2157,17 @@ class _ConfirmField { final _agentActions = >{ ActiveAgent.health: [ _AgentAction( - label: '查看健康情况', + label: '健康概览', icon: Icons.trending_up, isWide: true, route: 'trend', ), + _AgentAction( + label: '蓝牙录入', + icon: Icons.bluetooth, + isWide: true, + route: 'devices', + ), ], ActiveAgent.diet: [ _AgentAction( @@ -2446,30 +2306,52 @@ class _ExpandableAdviceState extends State<_ExpandableAdvice> { class _EditFieldCell extends StatefulWidget { final String initialValue; final ValueChanged onDone; - const _EditFieldCell({super.key, required this.initialValue, required this.onDone}); + const _EditFieldCell({ + super.key, + required this.initialValue, + required this.onDone, + }); - @override State<_EditFieldCell> createState() => _EditFieldCellState(); + @override + State<_EditFieldCell> createState() => _EditFieldCellState(); } class _EditFieldCellState extends State<_EditFieldCell> { late final TextEditingController _ctrl; - @override void initState() { + @override + void initState() { super.initState(); _ctrl = TextEditingController(text: widget.initialValue); } - @override void dispose() { _ctrl.dispose(); super.dispose(); } + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } - @override Widget build(BuildContext context) => TextField( + @override + Widget build(BuildContext context) => TextField( controller: _ctrl, autofocus: true, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1E293B)), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF1E293B), + ), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - filled: true, fillColor: Colors.white, - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: const Color(0xFF6366F1).withAlpha(80))), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF6366F1), width: 1.5)), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: const Color(0xFF6366F1).withAlpha(80)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF6366F1), width: 1.5), + ), ), onSubmitted: widget.onDone, onTapOutside: (_) => widget.onDone(_ctrl.text), diff --git a/health_app/lib/pages/medication/medication_list_page.dart b/health_app/lib/pages/medication/medication_list_page.dart index f840d38..bb7e2bb 100644 --- a/health_app/lib/pages/medication/medication_list_page.dart +++ b/health_app/lib/pages/medication/medication_list_page.dart @@ -55,7 +55,7 @@ class _MedicationListPageState extends ConsumerState @override Widget build(BuildContext context) { - return Scaffold( + return GradientScaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -79,7 +79,7 @@ class _MedicationListPageState extends ConsumerState ), floatingActionButton: FloatingActionButton( onPressed: () => pushRoute(ref, 'medicationEdit'), - backgroundColor: AppColors.primary, + backgroundColor: AppColors.primaryDark, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: const Icon(Icons.add, size: 28, color: Colors.white), ), @@ -119,9 +119,9 @@ class _MedicationListPageState extends ConsumerState child: Container( padding: const EdgeInsets.all(AppTheme.sLg), decoration: BoxDecoration( - color: AppTheme.surface, - borderRadius: BorderRadius.circular(AppTheme.rMd), - border: Border.all(color: AppColors.border), + gradient: AppColors.surfaceGradient, + borderRadius: BorderRadius.circular(AppTheme.rLg), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -130,15 +130,16 @@ class _MedicationListPageState extends ConsumerState width: 44, height: 44, decoration: BoxDecoration( - color: AppColors.iconBg, - borderRadius: BorderRadius.circular(AppTheme.rSm), + gradient: isActive + ? AppColors.primaryGradient + : AppColors.surfaceGradient, + borderRadius: BorderRadius.circular(AppTheme.rMd), + border: Border.all(color: AppColors.borderLight), ), child: Icon( Icons.medication_outlined, size: 25, - color: isActive - ? AppColors.primary - : AppTheme.textSub, + color: isActive ? Colors.white : AppTheme.textSub, ), ), const SizedBox(width: AppTheme.sMd), diff --git a/health_app/lib/pages/profile/profile_page.dart b/health_app/lib/pages/profile/profile_page.dart index ba88834..143a822 100644 --- a/health_app/lib/pages/profile/profile_page.dart +++ b/health_app/lib/pages/profile/profile_page.dart @@ -17,7 +17,7 @@ class ProfilePage extends ConsumerWidget { return GradientScaffold( appBar: AppBar( - backgroundColor: AppColors.cardBackground, + backgroundColor: Colors.white.withValues(alpha: 0.9), title: const Text('个人信息'), ), body: SafeArea( @@ -31,7 +31,7 @@ class ProfilePage extends ConsumerWidget { Center( child: CircleAvatar( radius: 50, - backgroundColor: AppColors.cardInner, + backgroundColor: AppColors.primaryLight, backgroundImage: user?.avatarUrl != null ? NetworkImage(user!.avatarUrl!) : null, @@ -60,9 +60,9 @@ class ProfilePage extends ConsumerWidget { vertical: 6, ), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -92,9 +92,9 @@ class ProfilePage extends ConsumerWidget { width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: AppColors.cardBackground, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(AppTheme.rLg), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Column( @@ -159,10 +159,11 @@ class ProfilePage extends ConsumerWidget { width: 40, height: 40, decoration: BoxDecoration( - color: AppColors.iconBg, - borderRadius: BorderRadius.circular(10), + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: AppColors.cardShadowLight, ), - child: Icon(icon, size: 22, color: AppColors.primary), + child: Icon(icon, size: 22, color: Colors.white), ), const SizedBox(width: 14), Expanded( diff --git a/health_app/lib/pages/remaining_pages.dart b/health_app/lib/pages/remaining_pages.dart index 8493ca8..ac94145 100644 --- a/health_app/lib/pages/remaining_pages.dart +++ b/health_app/lib/pages/remaining_pages.dart @@ -68,8 +68,7 @@ class _DietRecordListPageState extends ConsumerState { @override Widget build(BuildContext context) { if (_loading) - return Scaffold( - backgroundColor: AppColors.background, + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -92,8 +91,7 @@ class _DietRecordListPageState extends ConsumerState { (s, r) => s + ((r['totalCalories'] as num?)?.toInt() ?? 0), ); - return Scaffold( - backgroundColor: AppColors.background, + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1349,8 +1347,7 @@ class _HealthArchivePageState extends ConsumerState { @override Widget build(BuildContext context) { if (_loading) { - return Scaffold( - backgroundColor: AppColors.background, + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1367,8 +1364,7 @@ class _HealthArchivePageState extends ConsumerState { body: const Center(child: CircularProgressIndicator()), ); } - return Scaffold( - backgroundColor: AppColors.background, + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1856,8 +1852,7 @@ class _HealthCalendarPageState extends ConsumerState { final isToday = _selectedDate != null && _dateKey(_selectedDate!) == _dateKey(today); - return Scaffold( - backgroundColor: AppColors.background, + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, diff --git a/health_app/lib/providers/chat_provider.dart b/health_app/lib/providers/chat_provider.dart index 9cab33e..9527ec6 100644 --- a/health_app/lib/providers/chat_provider.dart +++ b/health_app/lib/providers/chat_provider.dart @@ -27,7 +27,15 @@ class ChatMessage { bool get isUser => role == 'user'; } -enum ActiveAgent { default_, consultation, health, diet, medication, report, exercise } +enum ActiveAgent { + default_, + consultation, + health, + diet, + medication, + report, + exercise, +} class ChatState { final ActiveAgent activeAgent; @@ -42,15 +50,19 @@ class ChatState { this.isStreaming = false, this.thinkingText, }); - ChatState copyWith({ActiveAgent? activeAgent, List? messages, - String? conversationId, bool? isStreaming, String? thinkingText}) => - ChatState( - activeAgent: activeAgent ?? this.activeAgent, - messages: messages ?? this.messages, - conversationId: conversationId ?? this.conversationId, - isStreaming: isStreaming ?? this.isStreaming, - thinkingText: thinkingText ?? this.thinkingText, - ); + ChatState copyWith({ + ActiveAgent? activeAgent, + List? messages, + String? conversationId, + bool? isStreaming, + String? thinkingText, + }) => ChatState( + activeAgent: activeAgent ?? this.activeAgent, + messages: messages ?? this.messages, + conversationId: conversationId ?? this.conversationId, + isStreaming: isStreaming ?? this.isStreaming, + thinkingText: thinkingText ?? this.thinkingText, + ); } class SelectedAgentNotifier extends Notifier { @@ -60,18 +72,29 @@ class SelectedAgentNotifier extends Notifier { } final selectedAgentProvider = - NotifierProvider(SelectedAgentNotifier.new); -final chatProvider = NotifierProvider(ChatNotifier.new); + NotifierProvider( + SelectedAgentNotifier.new, + ); +final chatProvider = NotifierProvider( + ChatNotifier.new, +); ActiveAgent _parseAgent(String? type) { switch (type?.toLowerCase()) { - case 'consultation': return ActiveAgent.consultation; - case 'health': return ActiveAgent.health; - case 'diet': return ActiveAgent.diet; - case 'medication': return ActiveAgent.medication; - case 'report': return ActiveAgent.report; - case 'exercise': return ActiveAgent.exercise; - default: return ActiveAgent.default_; + case 'consultation': + return ActiveAgent.consultation; + case 'health': + return ActiveAgent.health; + case 'diet': + return ActiveAgent.diet; + case 'medication': + return ActiveAgent.medication; + case 'report': + return ActiveAgent.report; + case 'exercise': + return ActiveAgent.exercise; + default: + return ActiveAgent.default_; } } @@ -85,7 +108,10 @@ class ChatNotifier extends Notifier { void confirmMessage(String id) { final msgs = state.messages.toList(); final i = msgs.indexWhere((m) => m.id == id); - if (i >= 0) { msgs[i].confirmed = true; state = state.copyWith(messages: msgs); } + if (i >= 0) { + msgs[i].confirmed = true; + state = state.copyWith(messages: msgs); + } ref.invalidate(medicationListProvider); ref.invalidate(latestHealthProvider); } @@ -93,7 +119,10 @@ class ChatNotifier extends Notifier { void startEditingField(String msgId, String fieldLabel) { final msgs = state.messages.toList(); final i = msgs.indexWhere((m) => m.id == msgId); - if (i >= 0) { msgs[i].metadata?['_editingField'] = fieldLabel; state = state.copyWith(messages: msgs); } + if (i >= 0) { + msgs[i].metadata?['_editingField'] = fieldLabel; + state = state.copyWith(messages: msgs); + } } void finishEditingField(String msgId, String fieldLabel, String value) { @@ -116,13 +145,18 @@ class ChatNotifier extends Notifier { void insertTaskCard() { if (state.messages.any((m) => m.type == MessageType.taskCard)) return; - state = state.copyWith(messages: [ChatMessage( - id: 'task_card', - role: 'assistant', - content: '', - createdAt: DateTime.now(), - type: MessageType.taskCard, - ), ...state.messages]); + state = state.copyWith( + messages: [ + ChatMessage( + id: 'task_card', + role: 'assistant', + content: '', + createdAt: DateTime.now(), + type: MessageType.taskCard, + ), + ...state.messages, + ], + ); } void setAgent(ActiveAgent a) { @@ -175,7 +209,9 @@ class ChatNotifier extends Notifier { id: map['id']?.toString() ?? '', role: map['role']?.toString() ?? 'user', content: map['content']?.toString() ?? '', - createdAt: DateTime.tryParse(map['createdAt']?.toString() ?? '') ?? DateTime.now(), + createdAt: + DateTime.tryParse(map['createdAt']?.toString() ?? '') ?? + DateTime.now(), type: MessageType.text, ); }).toList(); @@ -190,14 +226,19 @@ class ChatNotifier extends Notifier { } void insertAgentWelcome(ActiveAgent agent) { - state = state.copyWith(messages: [...state.messages, ChatMessage( - id: 'welcome_${agent.name}_${DateTime.now().millisecondsSinceEpoch}', - role: 'assistant', - content: '', - createdAt: DateTime.now(), - type: MessageType.agentWelcome, - metadata: {'agent': agent.name}, - )]); + state = state.copyWith( + messages: [ + ...state.messages, + ChatMessage( + id: 'welcome_${agent.name}_${DateTime.now().millisecondsSinceEpoch}', + role: 'assistant', + content: '', + createdAt: DateTime.now(), + type: MessageType.agentWelcome, + metadata: {'agent': agent.name}, + ), + ], + ); } /// 点击胶囊:先出用户标签 → 0.4 秒后出欢迎卡片,不走 AI @@ -275,7 +316,9 @@ class ChatNotifier extends Notifier { createdAt: DateTime.now(), ); state = state.copyWith( - messages: [...state.messages, userMsg], isStreaming: true); + messages: [...state.messages, userMsg], + isStreaming: true, + ); await _sendToAI(text); } @@ -289,7 +332,10 @@ class ChatNotifier extends Notifier { ); // 立即加入空 AI 消息,让思考动画有载体 - state = state.copyWith(messages: [...state.messages, aiMsg], isStreaming: true); + state = state.copyWith( + messages: [...state.messages, aiMsg], + isStreaming: true, + ); try { final token = await ref.read(apiClientProvider).accessToken; @@ -309,25 +355,25 @@ class ChatNotifier extends Notifier { await for (final event in stream) { _processEvent(event, aiMsg); } + if (state.isStreaming) { + _done(aiMsg); + } } catch (e) { _addError(aiMsg, '网络异常,请稍后重试'); } } void _addError(ChatMessage aiMsg, String errorText) { - state = state.copyWith( - messages: [ - ...state.messages, - ChatMessage( - id: 'err_${DateTime.now().millisecondsSinceEpoch}', - role: 'assistant', - content: errorText, - createdAt: DateTime.now(), - ), - ], - isStreaming: false, - thinkingText: null, - ); + aiMsg.content = errorText; + aiMsg.type = MessageType.text; + final u = state.messages.toList(); + final i = u.indexWhere((x) => x.id == aiMsg.id); + if (i >= 0) { + u[i] = aiMsg; + } else { + u.add(aiMsg); + } + state = state.copyWith(messages: u, isStreaming: false, thinkingText: null); } void _processEvent(Map j, ChatMessage aiMsg) { @@ -385,7 +431,16 @@ class ChatNotifier extends Notifier { void _done(ChatMessage m) { final u = state.messages.toList(); - if (!u.any((x) => x.id == m.id) && m.content.isNotEmpty) u.add(m); + final content = m.content.trim(); + if (content.isEmpty && m.type == MessageType.text) { + m.content = '暂时没有收到回复,请稍后再试。'; + } + final i = u.indexWhere((x) => x.id == m.id); + if (i >= 0) { + u[i] = m; + } else { + u.add(m); + } state = state.copyWith(messages: u, isStreaming: false, thinkingText: null); } } diff --git a/health_app/lib/widgets/admin_drawer.dart b/health_app/lib/widgets/admin_drawer.dart index 2f389a3..b957720 100644 --- a/health_app/lib/widgets/admin_drawer.dart +++ b/health_app/lib/widgets/admin_drawer.dart @@ -15,7 +15,7 @@ class AdminDrawer extends ConsumerWidget { return Drawer( width: MediaQuery.of(context).size.width * 0.8, child: Container( - color: AppColors.background, + decoration: const BoxDecoration(gradient: AppColors.drawerGradient), child: SafeArea( child: Column( children: [ @@ -23,10 +23,10 @@ class AdminDrawer extends ConsumerWidget { margin: const EdgeInsets.all(14), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.softGlassGradient, borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadow, ), child: Row( children: [ @@ -36,6 +36,7 @@ class AdminDrawer extends ConsumerWidget { decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(18), + boxShadow: AppColors.buttonShadow, ), child: const Icon( Icons.admin_panel_settings, @@ -135,7 +136,7 @@ class _MenuItem extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), child: Material( - color: selected ? AppColors.primarySoft : Colors.white, + color: Colors.transparent, borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), @@ -143,14 +144,22 @@ class _MenuItem extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( + gradient: selected + ? AppColors.primaryGradient + : AppColors.surfaceGradient, borderRadius: BorderRadius.circular(14), border: Border.all( - color: selected ? AppColors.primaryLight : AppColors.border, + color: selected + ? AppColors.primaryLight + : AppColors.borderLight, ), + boxShadow: selected + ? AppColors.buttonShadow + : AppColors.cardShadowLight, ), child: Row( children: [ - Icon(icon, color: color, size: 20), + Icon(icon, color: selected ? Colors.white : color, size: 20), const SizedBox(width: 12), Expanded( child: Text( @@ -158,7 +167,7 @@ class _MenuItem extends StatelessWidget { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: color, + color: selected ? Colors.white : color, ), ), ), diff --git a/health_app/lib/widgets/app_card.dart b/health_app/lib/widgets/app_card.dart index 49da871..0187701 100644 --- a/health_app/lib/widgets/app_card.dart +++ b/health_app/lib/widgets/app_card.dart @@ -34,9 +34,10 @@ class AppCard extends StatelessWidget { ), padding: padding, decoration: BoxDecoration( - color: backgroundColor ?? AppColors.cardBackground, + gradient: backgroundColor == null ? AppColors.surfaceGradient : null, + color: backgroundColor, borderRadius: radius, - border: border ?? Border.all(color: AppColors.border), + border: border ?? Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: child, diff --git a/health_app/lib/widgets/app_empty_state.dart b/health_app/lib/widgets/app_empty_state.dart index 555f990..a0b8322 100644 --- a/health_app/lib/widgets/app_empty_state.dart +++ b/health_app/lib/widgets/app_empty_state.dart @@ -27,11 +27,12 @@ class AppEmptyState extends StatelessWidget { width: 80, height: 80, decoration: BoxDecoration( - color: AppColors.iconBg, + gradient: AppColors.softGlassGradient, borderRadius: BorderRadius.circular(40), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadowLight, ), - child: Icon(icon, size: 34, color: AppColors.primary), + child: Icon(icon, size: 34, color: AppColors.primaryDark), ), const SizedBox(height: 18), Text( diff --git a/health_app/lib/widgets/app_menu_item.dart b/health_app/lib/widgets/app_menu_item.dart index fa1a164..541b04e 100644 --- a/health_app/lib/widgets/app_menu_item.dart +++ b/health_app/lib/widgets/app_menu_item.dart @@ -24,11 +24,11 @@ class AppMenuItem extends StatelessWidget { behavior: HitTestBehavior.opaque, child: Container( margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.border), + gradient: AppColors.surfaceGradient, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -37,10 +37,14 @@ class AppMenuItem extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: AppColors.iconBg, - borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.primaryLight, AppColors.infoLight], + ), + borderRadius: BorderRadius.circular(13), ), - child: Icon(icon, size: 21, color: AppColors.primary), + child: Icon(icon, size: 20, color: AppColors.primaryDark), ), const SizedBox(width: 14), Expanded( diff --git a/health_app/lib/widgets/app_tab_chip.dart b/health_app/lib/widgets/app_tab_chip.dart index ed92b21..f07266d 100644 --- a/health_app/lib/widgets/app_tab_chip.dart +++ b/health_app/lib/widgets/app_tab_chip.dart @@ -23,10 +23,13 @@ class AppTabChip extends StatelessWidget { margin: const EdgeInsets.only(right: AppTheme.sSm), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9), decoration: BoxDecoration( - color: selected ? AppColors.primary : Colors.white, + gradient: selected + ? AppColors.primaryGradient + : AppColors.surfaceGradient, + color: selected ? null : Colors.white, borderRadius: BorderRadius.circular(AppTheme.rPill), border: Border.all( - color: selected ? AppColors.primary : AppColors.border, + color: selected ? AppColors.primaryLight : AppColors.borderLight, ), boxShadow: selected ? AppColors.buttonShadow diff --git a/health_app/lib/widgets/common_widgets.dart b/health_app/lib/widgets/common_widgets.dart index a29423f..5918f79 100644 --- a/health_app/lib/widgets/common_widgets.dart +++ b/health_app/lib/widgets/common_widgets.dart @@ -23,7 +23,9 @@ class GradientBorderButton extends StatelessWidget { return Container( height: height, decoration: BoxDecoration( - gradient: isSuccess ? AppColors.primaryGradient : AppColors.primaryGradient, + gradient: isSuccess + ? AppColors.calmHealthGradient + : AppColors.primaryGradient, borderRadius: BorderRadius.circular(14), boxShadow: AppColors.buttonShadow, ), @@ -78,9 +80,10 @@ class CardActionButton extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - color: AppColors.cardInner, + gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadowLight, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -109,11 +112,19 @@ class SwipeDeleteTile extends StatefulWidget { final VoidCallback onDelete; final VoidCallback? onTap; final EdgeInsetsGeometry margin; - const SwipeDeleteTile({super.key, required this.child, required this.onDelete, this.onTap, this.margin = const EdgeInsets.symmetric(horizontal: 16, vertical: 4)}); - @override State createState() => _SwipeDeleteTileState(); + const SwipeDeleteTile({ + super.key, + required this.child, + required this.onDelete, + this.onTap, + this.margin = const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + }); + @override + State createState() => _SwipeDeleteTileState(); } -class _SwipeDeleteTileState extends State with SingleTickerProviderStateMixin { +class _SwipeDeleteTileState extends State + with SingleTickerProviderStateMixin { double _dx = 0; static const _maxSlide = 80.0; static const _threshold = 24.0; // 30% of 80 @@ -130,31 +141,53 @@ class _SwipeDeleteTileState extends State with SingleTickerProv } } - @override Widget build(BuildContext context) { + @override + Widget build(BuildContext context) { final isSwiped = _dx < 0; return Padding( padding: widget.margin, - child: Stack(children: [ - Positioned.fill(child: Container( - decoration: BoxDecoration(color: AppColors.error, borderRadius: BorderRadius.circular(16)), - child: const Align( - alignment: Alignment.centerRight, - child: Padding(padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: AppColors.textOnGradient, size: 28)), + child: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: AppColors.error, + borderRadius: BorderRadius.circular(16), + ), + child: const Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: 20), + child: Icon( + Icons.delete_outline, + color: AppColors.textOnGradient, + size: 28, + ), + ), + ), + ), ), - )), - GestureDetector( - behavior: isSwiped ? HitTestBehavior.opaque : HitTestBehavior.deferToChild, - onTap: isSwiped ? () { widget.onDelete(); setState(() => _dx = 0); } : widget.onTap, - onHorizontalDragUpdate: _onDragUpdate, - onHorizontalDragEnd: _onDragEnd, - child: Transform.translate( - offset: Offset(_dx, 0), - child: isSwiped - ? AbsorbPointer(absorbing: true, child: widget.child) - : widget.child, + GestureDetector( + behavior: isSwiped + ? HitTestBehavior.opaque + : HitTestBehavior.deferToChild, + onTap: isSwiped + ? () { + widget.onDelete(); + setState(() => _dx = 0); + } + : widget.onTap, + onHorizontalDragUpdate: _onDragUpdate, + onHorizontalDragEnd: _onDragEnd, + child: Transform.translate( + offset: Offset(_dx, 0), + child: isSwiped + ? AbsorbPointer(absorbing: true, child: widget.child) + : widget.child, + ), ), - ), - ]), + ], + ), ); } } @@ -180,14 +213,12 @@ class IconBox extends StatelessWidget { width: size, height: size, decoration: BoxDecoration( - color: backgroundColor ?? AppColors.iconBg, + gradient: backgroundColor == null ? AppColors.calmHealthGradient : null, + color: backgroundColor, borderRadius: BorderRadius.circular(size / 3), + boxShadow: AppColors.cardShadowLight, ), - child: Icon( - icon, - size: size * 0.55, - color: iconColor ?? AppColors.primary, - ), + child: Icon(icon, size: size * 0.55, color: iconColor ?? Colors.white), ); } } diff --git a/health_app/lib/widgets/doctor_drawer.dart b/health_app/lib/widgets/doctor_drawer.dart index e71cb46..877e34c 100644 --- a/health_app/lib/widgets/doctor_drawer.dart +++ b/health_app/lib/widgets/doctor_drawer.dart @@ -17,7 +17,7 @@ class DoctorDrawer extends ConsumerWidget { return Drawer( width: MediaQuery.of(context).size.width * 0.8, child: Container( - color: AppColors.background, + decoration: const BoxDecoration(gradient: AppColors.drawerGradient), child: SafeArea( child: Column( children: [ @@ -126,10 +126,10 @@ class _DrawerHeader extends StatelessWidget { margin: const EdgeInsets.all(14), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + gradient: AppColors.softGlassGradient, borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, + border: Border.all(color: AppColors.borderLight), + boxShadow: AppColors.cardShadow, ), child: InkWell( onTap: onTap, @@ -142,6 +142,7 @@ class _DrawerHeader extends StatelessWidget { decoration: BoxDecoration( gradient: AppColors.doctorGradient, borderRadius: BorderRadius.circular(18), + boxShadow: AppColors.buttonShadow, ), child: Icon(icon, color: Colors.white, size: 26), ), @@ -202,7 +203,7 @@ class _DrawerItem extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), child: Material( - color: selected ? AppColors.primarySoft : Colors.white, + color: Colors.transparent, borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), @@ -210,14 +211,22 @@ class _DrawerItem extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( + gradient: selected + ? AppColors.primaryGradient + : AppColors.surfaceGradient, borderRadius: BorderRadius.circular(14), border: Border.all( - color: selected ? AppColors.primaryLight : AppColors.border, + color: selected + ? AppColors.primaryLight + : AppColors.borderLight, ), + boxShadow: selected + ? AppColors.buttonShadow + : AppColors.cardShadowLight, ), child: Row( children: [ - Icon(icon, size: 20, color: color), + Icon(icon, size: 20, color: selected ? Colors.white : color), const SizedBox(width: 12), Expanded( child: Text( @@ -225,7 +234,7 @@ class _DrawerItem extends StatelessWidget { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: color, + color: selected ? Colors.white : color, ), ), ), diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index f3689a7..f044448 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/app_colors.dart'; -import '../core/app_theme.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/data_providers.dart'; @@ -17,39 +16,29 @@ class HealthDrawer extends ConsumerWidget { final latestHealth = ref.watch(latestHealthProvider); return Drawer( - width: MediaQuery.of(context).size.width * 0.85, + width: MediaQuery.of(context).size.width * 0.88, + backgroundColor: Colors.transparent, + elevation: 0, child: Container( - color: AppColors.background, + decoration: const BoxDecoration(gradient: AppColors.drawerGradient), child: SafeArea( - child: ListView( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 18), - children: [ - _ProfileCard(user: user, ref: ref), - const SizedBox(height: 12), - _QuickEntryGrid(ref: ref), - const SizedBox(height: 12), - _HealthSection(latestHealth: latestHealth, ref: ref), - const SizedBox(height: 12), - _FeatureSection(ref: ref), - const SizedBox(height: 12), - _Panel( - title: 'VIP服务', - icon: Icons.workspace_premium_rounded, - child: const ServicePackageCard(compact: true), - ), - const SizedBox(height: 12), - _Panel( - title: '保险', - icon: Icons.shield_rounded, - child: const Padding( - padding: EdgeInsets.fromLTRB(16, 4, 16, 16), - child: Text( - '即将上线,敬请期待', - style: TextStyle( - fontSize: 14, - color: AppColors.textSecondary, - ), - ), + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 22), + sliver: SliverList.list( + children: [ + _AccountHero(user: user, ref: ref), + const SizedBox(height: 14), + _PrimaryActions(ref: ref), + const SizedBox(height: 14), + _HealthDashboard(latestHealth: latestHealth, ref: ref), + const SizedBox(height: 14), + _NavigationSection(ref: ref), + const SizedBox(height: 14), + const _ServiceSection(), + ], ), ), ], @@ -60,99 +49,169 @@ class HealthDrawer extends ConsumerWidget { } } -class _ProfileCard extends StatelessWidget { +class _AccountHero extends StatelessWidget { final dynamic user; final WidgetRef ref; - const _ProfileCard({required this.user, required this.ref}); + const _AccountHero({required this.user, required this.ref}); @override Widget build(BuildContext context) { - final name = user?.name ?? '未设置昵称'; - final phone = user?.phone ?? '未登录'; + final name = (user?.name?.toString().isNotEmpty ?? false) + ? user.name.toString() + : '未设置昵称'; + final phone = (user?.phone?.toString().isNotEmpty ?? false) + ? user.phone.toString() + : '未登录'; + return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 16, 12, 16), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, - ), - child: Row( - children: [ - GestureDetector( - onTap: () => pushRoute(ref, 'profile'), - child: Container( - width: 52, - height: 52, - decoration: BoxDecoration( - gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(18), - ), - child: const Icon( - Icons.person_outline, - color: Colors.white, - size: 28, - ), - ), + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Colors.white, width: 1.8), + boxShadow: [ + BoxShadow( + color: const Color(0xFF6366F1).withValues(alpha: 0.12), + blurRadius: 30, + offset: const Offset(0, 16), ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () => pushRoute(ref, 'profile'), + child: Container( + width: 62, + height: 62, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(22), + ), + child: Container( + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.person_outline_rounded, + color: Colors.white, + size: 31, + ), ), - overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 3), - Text( - phone, - style: const TextStyle( - fontSize: 12, - color: AppColors.textSecondary, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'AI 健康管家', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.textHint, + ), + ), + const SizedBox(height: 4), + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 3), + Text( + phone, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + _CircleIconButton( + icon: Icons.settings_outlined, + onTap: () => pushRoute(ref, 'settings'), + ), + ], + ), + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Color(0xFFF7F4FF), Color(0xFFF2FBF9)], + ), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: const [ + Icon( + Icons.verified_user_outlined, + size: 18, + color: AppColors.primaryDark, + ), + SizedBox(width: 8), + Expanded( + child: Text( + '健康数据已开启隐私保护', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + ), ), ), ], ), ), - IconButton( - onPressed: () => pushRoute(ref, 'settings'), - icon: const Icon( - Icons.settings_outlined, - color: AppColors.textSecondary, - ), - ), ], ), ); } } -class _QuickEntryGrid extends StatelessWidget { +class _PrimaryActions extends StatelessWidget { final WidgetRef ref; - const _QuickEntryGrid({required this.ref}); + const _PrimaryActions({required this.ref}); @override Widget build(BuildContext context) { return Row( children: [ Expanded( - child: _QuickEntry( - icon: Icons.bluetooth_rounded, - label: '蓝牙设备', + child: _FeaturedAction( + icon: Icons.bluetooth_connected_rounded, + title: '蓝牙设备', + subtitle: '连接血压计', + gradient: AppColors.calmHealthGradient, onTap: () => pushRoute(ref, 'devices'), ), ), const SizedBox(width: 10), Expanded( - child: _QuickEntry( - icon: Icons.folder_outlined, - label: '健康档案', + child: _FeaturedAction( + icon: Icons.folder_shared_outlined, + title: '健康档案', + subtitle: '资料与病史', + gradient: AppColors.primaryGradient, onTap: () => pushRoute(ref, 'healthArchive'), ), ), @@ -161,176 +220,500 @@ class _QuickEntryGrid extends StatelessWidget { } } -class _QuickEntry extends StatelessWidget { +class _FeaturedAction extends StatelessWidget { final IconData icon; - final String label; + final String title; + final String subtitle; + final LinearGradient gradient; final VoidCallback onTap; - const _QuickEntry({ + const _FeaturedAction({ required this.icon, - required this.label, + required this.title, + required this.subtitle, + required this.gradient, required this.onTap, }); @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 15), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, - ), - child: Column( - children: [ - Icon(icon, size: 25, color: AppColors.primary), - const SizedBox(height: 6), - Text( - label, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - ), + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(22), + child: Container( + padding: const EdgeInsets.all(1.2), + decoration: BoxDecoration( + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(22), + boxShadow: AppColors.cardShadowLight, + ), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + borderRadius: BorderRadius.circular(20), ), - ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(15), + ), + child: Icon(icon, color: Colors.white, size: 22), + ), + const SizedBox(height: 12), + Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 3), + Text( + subtitle, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), ), ), ); } } -class _HealthSection extends StatelessWidget { +class _HealthDashboard extends StatelessWidget { final AsyncValue> latestHealth; final WidgetRef ref; - const _HealthSection({required this.latestHealth, required this.ref}); + const _HealthDashboard({required this.latestHealth, required this.ref}); @override Widget build(BuildContext context) { - return _Panel( + return _SectionShell( title: '健康仪表盘', - icon: Icons.dashboard_rounded, - trailing: GestureDetector( + subtitle: '最近一次健康数据', + icon: Icons.query_stats_rounded, + trailing: _TextPillButton( + label: '详情', onTap: () => pushRoute(ref, 'trend'), - child: const Text( - '详情', - style: TextStyle( - fontSize: 13, - color: AppColors.primary, - fontWeight: FontWeight.w700, - ), - ), ), child: latestHealth.when( - data: (data) => Padding( - padding: const EdgeInsets.fromLTRB(12, 4, 12, 14), - child: Row( - children: [ - _MiniMetric( - Icons.favorite_rounded, - '血压', - _bpText(data['BloodPressure']), - () => - pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), - ), - _MiniMetric( - Icons.monitor_heart_outlined, - '心率', - _metricVal(data['HeartRate']), - () => pushRoute(ref, 'trend', params: {'type': 'heart_rate'}), - ), - _MiniMetric( - Icons.bloodtype_outlined, - '血糖', - _metricVal(data['Glucose']), - () => pushRoute(ref, 'trend', params: {'type': 'glucose'}), - ), - ], - ), - ), + data: (data) => _DashboardBody(data: data, ref: ref), loading: () => const Padding( - padding: EdgeInsets.all(20), + padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), - error: (_, __) => Padding( - padding: const EdgeInsets.fromLTRB(12, 4, 12, 14), - child: Row( - children: [ - _MiniMetric(Icons.favorite_rounded, '血压', '--'), - _MiniMetric(Icons.monitor_heart_outlined, '心率', '--'), - _MiniMetric(Icons.bloodtype_outlined, '血糖', '--'), - ], - ), - ), + error: (_, __) => + _DashboardBody(data: const {}, ref: ref), ), ); } - - String _bpText(dynamic bp) { - if (bp is Map) - return '${bp['systolic'] ?? '--'}/${bp['diastolic'] ?? '--'}'; - return '--'; - } - - String _metricVal(dynamic val) { - if (val == null) return '--'; - if (val is num) return val.toStringAsFixed(val is double ? 1 : 0); - if (val is Map) return val['value']?.toString() ?? '--'; - return '--'; - } } -class _FeatureSection extends StatelessWidget { +class _DashboardBody extends StatelessWidget { + final Map data; final WidgetRef ref; - const _FeatureSection({required this.ref}); + const _DashboardBody({required this.data, required this.ref}); @override Widget build(BuildContext context) { - return _Panel( - title: '功能', - icon: Icons.apps_rounded, - child: Padding( - padding: const EdgeInsets.fromLTRB(12, 4, 12, 14), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FeatureChip( - icon: Icons.description_outlined, - label: '报告管理', - onTap: () => pushRoute(ref, 'reports'), + final bp = _bpText(data['BloodPressure']); + final metrics = [ + _MetricInfo( + icon: Icons.monitor_heart_outlined, + label: '心率', + value: _metricVal(data['HeartRate'], unit: 'bpm'), + routeType: 'heart_rate', + ), + _MetricInfo( + icon: Icons.water_drop_outlined, + label: '血糖', + value: _metricVal(data['Glucose'], unit: 'mmol/L'), + routeType: 'glucose', + ), + _MetricInfo( + icon: Icons.air_outlined, + label: '血氧', + value: _metricVal(data['SpO2'], unit: '%'), + routeType: 'spo2', + ), + _MetricInfo( + icon: Icons.monitor_weight_outlined, + label: '体重', + value: _metricVal(data['Weight'], unit: 'kg'), + routeType: 'weight', + ), + ]; + + return Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => + pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), + borderRadius: BorderRadius.circular(22), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(1.3), + decoration: BoxDecoration( + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(22), + ), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + gradient: AppColors.roseVioletGradient, + borderRadius: BorderRadius.circular(18), + ), + child: const Icon( + Icons.bloodtype_outlined, + color: Colors.white, + size: 25, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '血压', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 3), + Text( + bp, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + ], + ), + ), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: AppColors.textHint, + ), + ], + ), + ), ), - _FeatureChip( - icon: Icons.calendar_today_outlined, - label: '健康日历', - onTap: () => pushRoute(ref, 'calendar'), + ), + ), + const SizedBox(height: 10), + LayoutBuilder( + builder: (context, constraints) { + final width = (constraints.maxWidth - 10) / 2; + return Wrap( + spacing: 10, + runSpacing: 10, + children: metrics + .map( + (m) => _MetricTile( + info: m, + width: width, + onTap: () => pushRoute( + ref, + 'trend', + params: {'type': m.routeType}, + ), + ), + ) + .toList(), + ); + }, + ), + ], + ); + } + + static String _bpText(dynamic bp) { + if (bp is Map) { + final systolic = bp['systolic'] ?? bp['value'] ?? '--'; + final diastolic = bp['diastolic']; + if (diastolic == null) return systolic.toString(); + return '$systolic/$diastolic'; + } + return '--'; + } + + static String _metricVal(dynamic val, {String unit = ''}) { + if (val == null) return '--'; + dynamic raw = val; + if (val is Map) raw = val['value'] ?? val['val'] ?? val['data']; + if (raw == null) return '--'; + final text = raw is num + ? raw.toStringAsFixed(raw is double ? 1 : 0) + : raw.toString(); + if (text.isEmpty || text == '--') return '--'; + return unit.isEmpty ? text : '$text $unit'; + } +} + +class _MetricTile extends StatelessWidget { + final _MetricInfo info; + final double width; + final VoidCallback onTap; + const _MetricTile({ + required this.info, + required this.width, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFDFBFF), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.borderLight), ), - _FeatureChip( - icon: Icons.restaurant_outlined, - label: '饮食记录', - onTap: () => pushRoute(ref, 'dietRecords'), + child: Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: AppColors.primarySoft, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + info.icon, + size: 18, + color: AppColors.primaryDark, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + info.label, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: AppColors.textHint, + ), + ), + ], + ), + ), + ], ), - _FeatureChip( - icon: Icons.event_note_outlined, - label: '复查随访', - onTap: () => pushRoute(ref, 'followups'), - ), - ], + ), ), ), ); } } -class _Panel extends StatelessWidget { +class _NavigationSection extends StatelessWidget { + final WidgetRef ref; + const _NavigationSection({required this.ref}); + + @override + Widget build(BuildContext context) { + final items = [ + _NavItem( + icon: Icons.description_outlined, + title: '报告管理', + subtitle: '上传、查看和分析报告', + route: 'reports', + ), + _NavItem( + icon: Icons.medication_liquid_outlined, + title: '用药管理', + subtitle: '药品记录与服药打卡', + route: 'medications', + ), + _NavItem( + icon: Icons.restaurant_menu_outlined, + title: '饮食记录', + subtitle: '查看识别结果和历史饮食', + route: 'dietRecords', + ), + _NavItem( + icon: Icons.calendar_month_outlined, + title: '健康日历', + subtitle: '按日期查看健康记录', + route: 'calendar', + ), + _NavItem( + icon: Icons.event_note_outlined, + title: '复查随访', + subtitle: '随访计划和复查提醒', + route: 'followups', + ), + _NavItem( + icon: Icons.directions_run_outlined, + title: '运动计划', + subtitle: '计划、打卡和进度', + route: 'exercisePlan', + ), + ]; + + return _SectionShell( + title: '功能入口', + subtitle: '常用能力集中管理', + icon: Icons.apps_rounded, + child: Column( + children: [ + for (int i = 0; i < items.length; i++) ...[ + if (i > 0) const SizedBox(height: 8), + _NavigationRow( + item: items[i], + onTap: () => pushRoute(ref, items[i].route), + ), + ], + ], + ), + ); + } +} + +class _NavigationRow extends StatelessWidget { + final _NavItem item; + final VoidCallback onTap; + const _NavigationRow({required this.item, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11), + decoration: BoxDecoration( + color: const Color(0xFFFDFBFF), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.borderLight), + ), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + gradient: AppColors.actionOutlineGradient, + borderRadius: BorderRadius.circular(14), + ), + child: Icon(item.icon, size: 20, color: Colors.white), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + item.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 15, + color: AppColors.textHint, + ), + ], + ), + ), + ), + ); + } +} + +class _ServiceSection extends StatelessWidget { + const _ServiceSection(); + + @override + Widget build(BuildContext context) { + return _SectionShell( + title: '服务权益', + subtitle: '管理服务包和随访权益', + icon: Icons.workspace_premium_outlined, + child: const ServicePackageCard(compact: true), + ); + } +} + +class _SectionShell extends StatelessWidget { final String title; + final String subtitle; final IconData icon; final Widget child; final Widget? trailing; - const _Panel({ + const _SectionShell({ required this.title, + required this.subtitle, required this.icon, required this.child, this.trailing, @@ -339,42 +722,56 @@ class _Panel extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(AppTheme.rLg), - border: Border.all(color: AppColors.border), + color: Colors.white.withValues(alpha: 0.96), + borderRadius: BorderRadius.circular(26), + border: Border.all(color: Colors.white, width: 1.5), boxShadow: AppColors.cardShadowLight, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 6), - child: Row( - children: [ - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: AppColors.iconBg, - borderRadius: BorderRadius.circular(10), - ), - child: Icon(icon, size: 17, color: AppColors.primary), + Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + gradient: AppColors.primaryGradient, + borderRadius: BorderRadius.circular(14), ), - const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - ), + child: Icon(icon, size: 20, color: Colors.white), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], ), - const Spacer(), - if (trailing != null) trailing!, - ], - ), + ), + if (trailing != null) trailing!, + ], ), + const SizedBox(height: 14), child, ], ), @@ -382,89 +779,88 @@ class _Panel extends StatelessWidget { } } -class _MiniMetric extends StatelessWidget { +class _TextPillButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + const _TextPillButton({required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(999), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + color: AppColors.primarySoft, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: AppColors.borderLight), + ), + child: Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w900, + color: AppColors.primaryDark, + ), + ), + ), + ), + ); + } +} + +class _CircleIconButton extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + const _CircleIconButton({required this.icon, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFF7F4FF), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.borderLight), + ), + child: Icon(icon, size: 21, color: AppColors.primaryDark), + ), + ), + ); + } +} + +class _MetricInfo { final IconData icon; final String label; final String value; - final VoidCallback? onTap; - const _MiniMetric(this.icon, this.label, this.value, [this.onTap]); - - @override - Widget build(BuildContext context) { - return Expanded( - child: GestureDetector( - onTap: onTap, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 3), - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: AppColors.cardInner, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.borderLight), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 22, color: AppColors.primary), - const SizedBox(height: 5), - Text( - value, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - ), - ), - Text( - label, - style: const TextStyle(fontSize: 11, color: AppColors.textHint), - ), - ], - ), - ), - ), - ); - } -} - -class _FeatureChip extends StatelessWidget { - final IconData icon; - final String label; - final VoidCallback onTap; - const _FeatureChip({ + final String routeType; + const _MetricInfo({ required this.icon, required this.label, - required this.onTap, + required this.value, + required this.routeType, + }); +} + +class _NavItem { + final IconData icon; + final String title; + final String subtitle; + final String route; + const _NavItem({ + required this.icon, + required this.title, + required this.subtitle, + required this.route, }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 92, - padding: const EdgeInsets.symmetric(vertical: 11), - decoration: BoxDecoration( - color: AppColors.cardInner, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.borderLight), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 21, color: AppColors.primary), - const SizedBox(height: 5), - Text( - label, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - ), - ), - ], - ), - ), - ); - } } diff --git a/start-dev.bat b/start-dev.bat index 353fcde..ff109c0 100644 --- a/start-dev.bat +++ b/start-dev.bat @@ -2,7 +2,7 @@ chcp 65001 >nul title HealthManager -set "PATH=D:\PostgreSQL\18\pgsql\bin;%PATH%" +set "PATH=D:\PostgreSQL\18\pgsql\bin;D:\Android\platform-tools;%PATH%" echo ======================================== echo HealthManager - Start Services @@ -14,11 +14,16 @@ echo [1/2] Starting PostgreSQL... if errorlevel 1 (echo PG already running) else (echo PG started) echo. -echo [2/2] Starting .NET Backend (http://192.168.1.29:5000)... +echo [2/3] Starting .NET Backend... start "Backend" cmd /k "cd /d D:\health_project\backend\src\Health.WebApi && dotnet run" echo Waiting for backend... timeout /t 8 /nobreak >nul echo. -echo All services started. Open the app on your phone. +echo [3/3] Setup phone connection... +adb reverse tcp:5000 tcp:5000 2>nul +if errorlevel 1 (echo USB not connected, skip) else (echo Phone -> PC forwarding OK) +echo. + +echo All done. Open the app on your phone. pause