diff --git a/health_app/assets/branding/page_calendar_scene.png b/health_app/assets/branding/page_calendar_scene.png new file mode 100644 index 0000000..96ad568 Binary files /dev/null and b/health_app/assets/branding/page_calendar_scene.png differ diff --git a/health_app/assets/branding/page_exercise_scene.png b/health_app/assets/branding/page_exercise_scene.png new file mode 100644 index 0000000..408d9cc Binary files /dev/null and b/health_app/assets/branding/page_exercise_scene.png differ diff --git a/health_app/assets/branding/page_health_archive_scene.png b/health_app/assets/branding/page_health_archive_scene.png new file mode 100644 index 0000000..f0b1139 Binary files /dev/null and b/health_app/assets/branding/page_health_archive_scene.png differ diff --git a/health_app/assets/branding/page_medication_scene.png b/health_app/assets/branding/page_medication_scene.png new file mode 100644 index 0000000..82fe408 Binary files /dev/null and b/health_app/assets/branding/page_medication_scene.png differ diff --git a/health_app/assets/branding/page_notifications_scene.png b/health_app/assets/branding/page_notifications_scene.png new file mode 100644 index 0000000..d548feb Binary files /dev/null and b/health_app/assets/branding/page_notifications_scene.png differ diff --git a/health_app/assets/branding/page_profile_scene.png b/health_app/assets/branding/page_profile_scene.png new file mode 100644 index 0000000..ea4feab Binary files /dev/null and b/health_app/assets/branding/page_profile_scene.png differ diff --git a/health_app/assets/branding/page_report_scene.png b/health_app/assets/branding/page_report_scene.png new file mode 100644 index 0000000..80466d4 Binary files /dev/null and b/health_app/assets/branding/page_report_scene.png differ diff --git a/health_app/assets/branding/ui_abstract_header_bg_v1.png b/health_app/assets/branding/ui_abstract_header_bg_v1.png new file mode 100644 index 0000000..5797b31 Binary files /dev/null and b/health_app/assets/branding/ui_abstract_header_bg_v1.png differ diff --git a/health_app/assets/branding/ui_corner_accent_v1.png b/health_app/assets/branding/ui_corner_accent_v1.png new file mode 100644 index 0000000..b49cc81 Binary files /dev/null and b/health_app/assets/branding/ui_corner_accent_v1.png differ diff --git a/health_app/assets/branding/ui_empty_cluster_v1.png b/health_app/assets/branding/ui_empty_cluster_v1.png new file mode 100644 index 0000000..1a640fb Binary files /dev/null and b/health_app/assets/branding/ui_empty_cluster_v1.png differ diff --git a/health_app/assets/branding/ui_neutral_cluster_v1.png b/health_app/assets/branding/ui_neutral_cluster_v1.png new file mode 100644 index 0000000..658f641 Binary files /dev/null and b/health_app/assets/branding/ui_neutral_cluster_v1.png differ diff --git a/health_app/lib/core/api_client.dart b/health_app/lib/core/api_client.dart index f0c9447..4855b54 100644 --- a/health_app/lib/core/api_client.dart +++ b/health_app/lib/core/api_client.dart @@ -6,7 +6,7 @@ import 'local_database.dart'; /// API 基础地址。可通过 --dart-define=API_BASE_URL=http://host:5000 覆盖。 const String baseUrl = String.fromEnvironment( 'API_BASE_URL', - defaultValue: 'http://10.4.191.129:5000', + defaultValue: 'http://10.4.202.36:5000', ); class ApiException implements Exception { diff --git a/health_app/lib/core/app_colors.dart b/health_app/lib/core/app_colors.dart index 09d7c7e..f445729 100644 --- a/health_app/lib/core/app_colors.dart +++ b/health_app/lib/core/app_colors.dart @@ -5,37 +5,37 @@ class AppColors { AppColors._(); static const Color primary = Color(0xFF7C5CFF); - static const Color primaryLight = Color(0xFFEDE8FF); - static const Color primarySoft = Color(0xFFF6F3FF); - static const Color primaryDark = Color(0xFF5B3FE8); - static const Color blueMeasure = Color(0xFF4F7CFF); + static const Color primaryLight = Color(0xFFEDE9FF); + static const Color primarySoft = Color(0xFFF7F4FF); + static const Color primaryDark = Color(0xFF5B4BDB); + static const Color blueMeasure = Color(0xFF60A5FA); 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 auraLavender = Color(0xFFC4B5FD); + static const Color auraIndigo = Color(0xFF8B5CF6); + static const Color auraOrchid = Color(0xFFE879F9); + static const Color auraDeepIndigo = Color(0xFF7C3AED); + static const Color mintAccent = Color(0xFF67E8F9); + static const Color peachAccent = Color(0xFFFBCFE8); + static const Color roseAccent = Color(0xFFFB7185); + static const Color meadowAccent = Color(0xFFBAE6FD); + static const Color sageAccent = Color(0xFFE9D5FF); - static const Color background = Color(0xFFFAF8FF); - static const Color backgroundSoft = Color(0xFFFFFCFF); + static const Color background = Color(0xFFF8FAFF); + static const Color backgroundSoft = Color(0xFFFFFFFF); static const Color cardBackground = Color(0xFFFFFFFF); - static const Color cardInner = Color(0xFFF7F5FF); + static const Color cardInner = Color(0xFFF1F5F9); static const Color pageGrey = background; - static const Color iconBg = Color(0xFFF2EEFF); - static const Color avatarBg = Color(0xFFF4EFFF); + static const Color iconBg = Color(0xFFEFF6FF); + static const Color avatarBg = Color(0xFFF5F3FF); static const Color textPrimary = Color(0xFF1F2937); - static const Color textSecondary = Color(0xFF667085); - static const Color textHint = Color(0xFF98A2B3); + static const Color textSecondary = Color(0xFF475467); + static const Color textHint = Color(0xFF667085); static const Color textOnGradient = Color(0xFFFFFFFF); - static const Color border = Color(0xFFE5E7EB); - static const Color borderLight = Color(0xFFF1F3F8); - static const Color divider = Color(0xFFEFF2F7); + static const Color border = Color(0xFFD5DCE5); + static const Color borderLight = Color(0xFFE2E8F0); + static const Color divider = Color(0xFFE5EAF1); static const Color success = Color(0xFF16A34A); static const Color successLight = Color(0xFFEAF8EF); @@ -56,10 +56,10 @@ class AppColors { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - Color(0xFFFFFCFF), - Color(0xFFF9F5FF), - Color(0xFFF5F1FF), - Color(0xFFF2F6FF), + Color(0xFFFFFFFF), + Color(0xFFEFF6FF), + Color(0xFFF5F3FF), + Color(0xFFFDF2F8), ], stops: [0.0, 0.42, 0.74, 1.0], ); @@ -67,63 +67,68 @@ class AppColors { static const LinearGradient drawerGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFFFFFFF), Color(0xFFFBF7FF), Color(0xFFF0F4FF)], + colors: [Color(0xFFFFFFFF), Color(0xFFEFF6FF), Color(0xFFFDF2F8)], stops: [0.0, 0.52, 1.0], ); static const LinearGradient surfaceGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFFFFFFF), Color(0xFFFCFAFF)], + colors: [Color(0xFFFFFFFF), Color(0xFFF8FAFC)], ); static const LinearGradient softGlassGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xF7FFFFFF), Color(0xECFFFFFF)], + colors: [Color(0xFAFFFFFF), Color(0xF0FFFFFF)], ); static const LinearGradient roseVioletGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF8B5CF6), Color(0xFFF43F5E)], + colors: [Color(0xFF8B5CF6), Color(0xFFFB7185)], ); static const LinearGradient sageVioletGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF9CD9C9), Color(0xFFAE2EF9)], + colors: [Color(0xFF60A5FA), Color(0xFFE879F9)], ); static const LinearGradient meadowVioletGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF72F874), Color(0xFF8C6BD9)], + colors: [Color(0xFFBAE6FD), Color(0xFFC4B5FD)], ); static const LinearGradient calmHealthGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF9CD9C9), Color(0xFF818CF8)], + colors: [Color(0xFF60A5FA), Color(0xFFC4B5FD)], ); static const LinearGradient warmCareGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFFFA6A6), Color(0xFFC084FC)], + colors: [Color(0xFFFBCFE8), Color(0xFFC4B5FD)], ); static const LinearGradient actionOutlineGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFC084FC), Color(0xFF818CF8), Color(0xFFC084FC), Color(0xFF818CF8)], + colors: [ + Color(0xFFC084FC), + Color(0xFF818CF8), + Color(0xFFC084FC), + Color(0xFF818CF8), + ], stops: [0.0, 0.3, 0.6, 1.0], ); static const LinearGradient doctorGradient = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF4F7CFF), Color(0xFF7C5CFF)], + colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6)], ); static List get cardShadow => [ diff --git a/health_app/lib/core/app_router.dart b/health_app/lib/core/app_router.dart index 688bd50..0b9a782 100644 --- a/health_app/lib/core/app_router.dart +++ b/health_app/lib/core/app_router.dart @@ -66,6 +66,8 @@ Widget buildPage(RouteInfo route, WidgetRef ref) { ); case 'exercisePlan': return const ExercisePlanPage(); + case 'exercisePlanDetail': + return ExercisePlanDetailPage(id: params['id'] ?? ''); case 'exerciseCreate': return const ExercisePlanCreatePage(); case 'dietRecords': diff --git a/health_app/lib/pages/doctor/doctor_report_detail_page.dart b/health_app/lib/pages/doctor/doctor_report_detail_page.dart index 763e548..1f66170 100644 --- a/health_app/lib/pages/doctor/doctor_report_detail_page.dart +++ b/health_app/lib/pages/doctor/doctor_report_detail_page.dart @@ -305,7 +305,9 @@ class _DoctorReportDetailPageState SizedBox( width: double.infinity, child: OutlinedButton.icon( - onPressed: () {}, // TODO: 打开文件查看器 + onPressed: () { + pushRoute(ref, 'reportOriginal', params: {'url': fileUrl!, 'title': '原始报告'}); + }, icon: const Icon(Icons.visibility, size: 18), label: const Text('查看原始报告'), style: OutlinedButton.styleFrom( diff --git a/health_app/lib/pages/doctor/doctor_settings_page.dart b/health_app/lib/pages/doctor/doctor_settings_page.dart index 984405d..8cc8169 100644 --- a/health_app/lib/pages/doctor/doctor_settings_page.dart +++ b/health_app/lib/pages/doctor/doctor_settings_page.dart @@ -28,8 +28,8 @@ class DoctorSettingsPage extends ConsumerWidget { '推送通知', trailing: Switch(value: true, onChanged: (_) {}), ), - _SettingTile(Icons.description_outlined, '隐私政策', onTap: () {}), - _SettingTile(Icons.article_outlined, '用户协议', onTap: () {}), + _SettingTile(Icons.description_outlined, '隐私政策', onTap: () { pushRoute(ref, 'staticText', params: {'type': 'privacy'}); }), + _SettingTile(Icons.article_outlined, '用户协议', onTap: () { pushRoute(ref, 'staticText', params: {'type': 'terms'}); }), const SizedBox(height: 24), _SettingTile( Icons.delete_forever_outlined, diff --git a/health_app/lib/pages/medication/medication_list_page.dart b/health_app/lib/pages/medication/medication_list_page.dart index 00e443c..aefe903 100644 --- a/health_app/lib/pages/medication/medication_list_page.dart +++ b/health_app/lib/pages/medication/medication_list_page.dart @@ -7,6 +7,7 @@ import '../../providers/data_providers.dart'; import '../../widgets/app_empty_state.dart'; import '../../widgets/app_future_view.dart'; import '../../widgets/common_widgets.dart'; +import '../../widgets/enterprise_widgets.dart'; class MedicationListPage extends ConsumerStatefulWidget { const MedicationListPage({super.key}); @@ -14,35 +15,20 @@ class MedicationListPage extends ConsumerStatefulWidget { ConsumerState createState() => _MedicationListPageState(); } -class _MedicationListPageState extends ConsumerState - with SingleTickerProviderStateMixin { - String _filter = ''; +class _MedicationListPageState extends ConsumerState { + static const _medBlue = Color(0xFF60A5FA); + static const _medCyan = Color(0xFF8B5CF6); Future>>? _future; - late final _tabCtrl = TabController(length: 3, vsync: this); @override void initState() { super.initState(); - _tabCtrl.addListener(() { - if (!_tabCtrl.indexIsChanging) { - setState(() { - _filter = ['', 'active', 'inactive'][_tabCtrl.index]; - }); - _load(); - } - }); _load(); } - @override - void dispose() { - _tabCtrl.dispose(); - super.dispose(); - } - void _load() { setState(() { - _future = ref.read(medicationServiceProvider).getMedications(_filter); + _future = ref.read(medicationServiceProvider).getMedications(''); }); } @@ -52,8 +38,6 @@ class _MedicationListPageState extends ConsumerState _load(); } - static const _tabs = ['全部', '服用中', '已停药']; - @override Widget build(BuildContext context) { return GradientScaffold( @@ -63,43 +47,59 @@ class _MedicationListPageState extends ConsumerState onPressed: () => popRoute(ref), ), title: const Text('用药管理'), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(48), - child: TabBar( - controller: _tabCtrl, - indicatorColor: AppColors.textPrimary, - labelColor: AppColors.textPrimary, - unselectedLabelColor: AppColors.textHint, - labelStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - ), - tabs: _tabs.map((t) => Tab(child: Text(t))).toList(), - ), - ), ), floatingActionButton: FloatingActionButton( onPressed: () => pushRoute(ref, 'medicationEdit'), - backgroundColor: AppColors.primaryDark, + backgroundColor: _medBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: const Icon(Icons.add, size: 28, color: Colors.white), ), body: AppFutureView>>( - future: _future, - onRetry: _load, - errorTitle: '用药信息加载失败', - onData: (ctx, list) { - if (list.isEmpty) { - return const AppEmptyState( + future: _future, + onRetry: _load, + errorTitle: '用药信息加载失败', + onData: (ctx, list) { + if (list.isEmpty) { + return const AppEmptyState( + icon: Icons.medication_outlined, + title: '暂无用药', + subtitle: '点击右下角➕添加药品', + ); + } + final activeCount = list.where((m) => m['isActive'] == true).length; + final reminderCount = list.where((m) { + final times = m['timeOfDay'] as List?; + return times != null && times.isNotEmpty; + }).length; + return ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 88), + children: [ + EnterpriseHeader( + title: '用药管理', + subtitle: '集中管理服药计划、剂量和每日打卡状态', icon: Icons.medication_outlined, - title: '暂无用药', - subtitle: '点击右下角➕添加药品', - ); - } - return ListView.builder( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 80), - itemCount: list.length, - itemBuilder: (ctx, i) { + color: _medBlue, + accent: _medCyan, + stats: [ + EnterpriseStat( + label: '药品总数', + value: '${list.length} 个', + icon: Icons.inventory_2_outlined, + ), + EnterpriseStat( + label: '服用中', + value: '$activeCount 个', + icon: Icons.check_circle_outline, + ), + EnterpriseStat( + label: '有提醒', + value: '$reminderCount 个', + icon: Icons.alarm_outlined, + ), + ], + ), + const SizedBox(height: 10), + ...List.generate(list.length, (i) { final m = list[i]; final times = (m['timeOfDay'] as List?) @@ -112,22 +112,14 @@ class _MedicationListPageState extends ConsumerState key: Key(id), onDelete: () => _delete(id), onTap: () => pushRoute(ref, 'medCheckIn', params: {'id': id}), - margin: const EdgeInsets.symmetric( - horizontal: AppTheme.sLg, - vertical: 4, - ), + margin: const EdgeInsets.symmetric(vertical: 5), child: Container( padding: const EdgeInsets.all(AppTheme.sLg), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(AppTheme.rLg), - boxShadow: [ - BoxShadow( - color: const Color(0xFF101828).withValues(alpha: 0.08), - blurRadius: 18, - offset: const Offset(0, 6), - ), - ], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], ), child: Row( children: [ @@ -136,9 +128,13 @@ class _MedicationListPageState extends ConsumerState height: 44, decoration: BoxDecoration( gradient: isActive - ? AppColors.primaryGradient + ? const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [_medBlue, _medCyan], + ) : AppColors.surfaceGradient, - borderRadius: BorderRadius.circular(AppTheme.rMd), + borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.borderLight), ), child: Icon( @@ -172,14 +168,16 @@ class _MedicationListPageState extends ConsumerState vertical: 1, ), decoration: BoxDecoration( - color: AppTheme.bgSoft, - borderRadius: BorderRadius.circular(4), + color: _medBlue.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular( + 999, + ), ), child: Text( '已停', style: TextStyle( fontSize: 13, - color: AppTheme.textSub, + color: _medBlue, ), ), ), @@ -200,16 +198,17 @@ class _MedicationListPageState extends ConsumerState Icon( Icons.chevron_right, size: 21, - color: AppTheme.border, + color: AppColors.textHint, ), ], ), ), ); - }, - ); - }, - ), + }), + ], + ); + }, + ), ); } } diff --git a/health_app/lib/pages/notifications/notification_center_page.dart b/health_app/lib/pages/notifications/notification_center_page.dart index 5e0635d..df516bd 100644 --- a/health_app/lib/pages/notifications/notification_center_page.dart +++ b/health_app/lib/pages/notifications/notification_center_page.dart @@ -147,7 +147,7 @@ class _NotificationCenterPageState final unread = _history?.unreadCount ?? 0; return Scaffold( - backgroundColor: const Color(0xFFF6F8FC), + backgroundColor: const Color(0xFFF8FAFF), appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -196,7 +196,10 @@ class _NotificationCenterPageState ), IconButton( tooltip: '通知设置', - icon: const Icon(Icons.settings_outlined, color: AppColors.textPrimary), + icon: const Icon( + Icons.settings_outlined, + color: AppColors.textPrimary, + ), onPressed: () => pushRoute(ref, 'notificationPrefs'), ), const SizedBox(width: 4), @@ -245,19 +248,19 @@ class _NotificationCenterPageState return ListView( physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), children: [ if (unread > 0) _UnreadHint(unreadCount: unread), - const SizedBox(height: 4), + const SizedBox(height: 8), if (today.isNotEmpty) ...[ const _SectionTitle('今天'), - const SizedBox(height: 4), + const SizedBox(height: 8), ...today.map(_buildDismissible), ], if (earlier.isNotEmpty) ...[ - if (today.isNotEmpty) const SizedBox(height: 12), + if (today.isNotEmpty) const SizedBox(height: 14), const _SectionTitle('更早'), - const SizedBox(height: 4), + const SizedBox(height: 8), ...earlier.map(_buildDismissible), ], ], @@ -265,7 +268,7 @@ class _NotificationCenterPageState } Widget _buildDismissible(InAppNotification item) => Padding( - padding: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.only(bottom: 10), child: Dismissible( key: ValueKey(item.id), direction: DismissDirection.endToStart, @@ -276,12 +279,9 @@ class _NotificationCenterPageState gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, - colors: [ - AppColors.error.withValues(alpha: 0.6), - AppColors.error, - ], + colors: [AppColors.error.withValues(alpha: 0.6), AppColors.error], ), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(16), ), child: const Row( mainAxisSize: MainAxisSize.min, @@ -345,15 +345,31 @@ class _SectionTitle extends StatelessWidget { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.fromLTRB(4, 12, 4, 6), - child: Text( - text, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: AppColors.textHint, - letterSpacing: 0.3, - ), + padding: const EdgeInsets.fromLTRB(2, 12, 2, 6), + child: Row( + children: [ + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(99), + ), + ), + const SizedBox(width: 8), + Text( + text, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + const SizedBox(width: 10), + const Expanded( + child: Divider(height: 1, thickness: 1, color: AppColors.divider), + ), + ], ), ); } @@ -369,19 +385,20 @@ class _NotificationCard extends StatelessWidget { final visual = _NotificationVisual.of(item); return Material( color: Colors.white, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(16), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(16), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(16), border: Border.all( color: item.isRead - ? const Color(0xFFEEF1F6) - : visual.color.withValues(alpha: 0.20), + ? AppColors.border + : visual.color.withValues(alpha: 0.34), + width: item.isRead ? 1 : 1.2, ), boxShadow: [ BoxShadow( @@ -399,13 +416,13 @@ class _NotificationCard extends StatelessWidget { clipBehavior: Clip.none, children: [ Container( - width: 36, - height: 36, + width: 44, + height: 44, decoration: BoxDecoration( color: visual.lightColor, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(12), ), - child: Icon(visual.icon, size: 18, color: visual.color), + child: Icon(visual.icon, size: 22, color: visual.color), ), if (!item.isRead) Positioned( @@ -423,7 +440,7 @@ class _NotificationCard extends StatelessWidget { ), ], ), - const SizedBox(width: 11), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -437,10 +454,10 @@ class _NotificationCard extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 14, + fontSize: 16, fontWeight: item.isRead - ? FontWeight.w600 - : FontWeight.w700, + ? FontWeight.w700 + : FontWeight.w800, color: AppColors.textPrimary, height: 1.3, ), @@ -450,19 +467,20 @@ class _NotificationCard extends StatelessWidget { Text( _formatTime(item.createdAt), style: const TextStyle( - fontSize: 11, - color: AppColors.textHint, + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, ), ), ], ), - const SizedBox(height: 2), + const SizedBox(height: 5), Text( item.message, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 12, + fontSize: 14, height: 1.45, color: AppColors.textSecondary, ), @@ -474,8 +492,8 @@ class _NotificationCard extends StatelessWidget { const SizedBox(width: 4), const Icon( Icons.chevron_right_rounded, - size: 18, - color: AppColors.textHint, + size: 22, + color: AppColors.textSecondary, ), ], ], @@ -514,8 +532,8 @@ class _NotificationVisual { case 'exercise': return const _NotificationVisual( LucideIcons.activity, - Color(0xFF10B981), - Color(0xFFD1FAE5), + Color(0xFF60A5FA), + Color(0xFFEFF6FF), '运动', ); case 'health': diff --git a/health_app/lib/pages/profile/profile_page.dart b/health_app/lib/pages/profile/profile_page.dart index 4767e75..4ea9ebc 100644 --- a/health_app/lib/pages/profile/profile_page.dart +++ b/health_app/lib/pages/profile/profile_page.dart @@ -4,6 +4,7 @@ import '../../core/app_colors.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; +import '../../widgets/enterprise_widgets.dart'; class ProfilePage extends ConsumerWidget { const ProfilePage({super.key}); @@ -34,10 +35,25 @@ class ProfilePage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _ProfileHero( - name: name, - phone: phone, - avatarUrl: user?.avatarUrl, + EnterpriseHeader( + title: name, + subtitle: phone.isNotEmpty ? phone : '未绑定手机', + icon: Icons.person_rounded, + color: const Color(0xFF38BDF8), + accent: const Color(0xFF8B5CF6), + stats: const [ + EnterpriseStat( + label: '账号状态', + value: '已登录', + icon: Icons.verified_user_rounded, + ), + EnterpriseStat( + label: '健康资料', + value: '可维护', + icon: Icons.folder_shared_rounded, + ), + ], + trailing: _AvatarBadge(avatarUrl: user?.avatarUrl), ), const SizedBox(height: 18), _InfoPanel( @@ -46,21 +62,21 @@ class ProfilePage extends ConsumerWidget { icon: Icons.badge_rounded, label: '姓名', value: name, - colors: const [Color(0xFF7DD3FC), Color(0xFF2563EB)], + colors: const [Color(0xFF7DD3FC), Color(0xFF38BDF8)], ), const _SoftDivider(), _InfoRow( icon: Icons.phone_iphone_rounded, label: '手机号', value: phone.isNotEmpty ? phone : '未绑定手机', - colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)], + colors: const [Color(0xFFFBCFE8), Color(0xFFF472B6)], ), const _SoftDivider(), _InfoRow( icon: Icons.folder_shared_rounded, label: '健康档案', value: '查看和维护基础健康资料', - colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)], + colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)], onTap: () => pushRoute(ref, 'healthArchive'), ), ], @@ -72,7 +88,7 @@ class ProfilePage extends ConsumerWidget { icon: Icons.verified_user_rounded, label: '隐私保护', value: '健康数据仅用于个人健康管理', - colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)], + colors: const [Color(0xFFFFB4A2), Color(0xFFFB7185)], ), ], ), @@ -131,94 +147,33 @@ class ProfilePage extends ConsumerWidget { } } -class _ProfileHero extends StatelessWidget { - final String name; - final String phone; +class _AvatarBadge extends StatelessWidget { final String? avatarUrl; - const _ProfileHero({ - required this.name, - required this.phone, - required this.avatarUrl, - }); + const _AvatarBadge({required this.avatarUrl}); @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(20), + width: 50, + height: 50, + padding: const EdgeInsets.all(2), decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFFEEF2FF), Color(0xFFF7ECFF), Color(0xFFEFFBF9)], - ), - borderRadius: BorderRadius.circular(30), - border: Border.all(color: Colors.white, width: 1.5), - boxShadow: [ - BoxShadow( - color: const Color(0xFF6D5DF6).withValues(alpha: 0.10), - blurRadius: 24, - offset: const Offset(0, 14), - ), - ], + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), ), - child: Row( - children: [ - Container( - width: 76, - height: 76, - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF38BDF8), Color(0xFFC084FC)], + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: avatarUrl != null + ? Image.network(avatarUrl!, fit: BoxFit.cover) + : const ColoredBox( + color: Colors.white, + child: Icon( + Icons.person_rounded, + color: Color(0xFF38BDF8), + size: 30, + ), ), - borderRadius: BorderRadius.circular(28), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(25), - child: avatarUrl != null - ? Image.network(avatarUrl!, fit: BoxFit.cover) - : const ColoredBox( - color: Colors.white, - child: Icon( - Icons.person_rounded, - color: Color(0xFF7C3AED), - size: 42, - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w900, - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 7), - Text( - phone.isNotEmpty ? phone : '未绑定手机', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.textSecondary, - ), - ), - ], - ), - ), - ], ), ); } @@ -233,9 +188,9 @@ class _InfoPanel extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.88), - borderRadius: BorderRadius.circular(26), - border: Border.all(color: Colors.white, width: 1.4), + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Column(children: children), @@ -261,7 +216,7 @@ class _InfoRow extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(18), + borderRadius: BorderRadius.circular(14), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12), child: Row( @@ -275,7 +230,14 @@ class _InfoRow extends StatelessWidget { end: Alignment.bottomRight, colors: colors, ), - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: colors.last.withValues(alpha: 0.18), + blurRadius: 12, + offset: const Offset(0, 5), + ), + ], ), child: Icon(icon, color: Colors.white, size: 22), ), @@ -324,6 +286,6 @@ class _SoftDivider extends StatelessWidget { @override Widget build(BuildContext context) { - return const Divider(height: 1, color: Color(0xFFF1F3F8)); + return const Divider(height: 1, color: AppColors.divider); } } diff --git a/health_app/lib/pages/remaining_pages.dart b/health_app/lib/pages/remaining_pages.dart index 9b602c8..31766ef 100644 --- a/health_app/lib/pages/remaining_pages.dart +++ b/health_app/lib/pages/remaining_pages.dart @@ -9,6 +9,7 @@ import '../providers/data_providers.dart'; import '../widgets/common_widgets.dart'; import '../widgets/app_error_state.dart'; import '../widgets/app_future_view.dart'; +import '../widgets/enterprise_widgets.dart'; /// 饮食记录列表(趋势+日历+胶囊详情+侧滑编辑删除) class DietRecordListPage extends ConsumerStatefulWidget { @@ -710,6 +711,18 @@ class _ExercisePlanPageState extends ConsumerState { _load(); } + String _todayKey() { + final now = DateTime.now(); + return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; + } + + String _slashDate(String value) { + if (value.length >= 10) { + return value.substring(0, 10).replaceAll('-', '/'); + } + return value.replaceAll('-', '/'); + } + @override Widget build(BuildContext context) { return GradientScaffold( @@ -724,19 +737,50 @@ class _ExercisePlanPageState extends ConsumerState { onPressed: () { pushRoute(ref, 'exerciseCreate'); }, - backgroundColor: AppTheme.primary, - child: const Icon(Icons.add), + backgroundColor: const Color(0xFF60A5FA), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: const Icon(Icons.add, color: Colors.white), ), body: AppFutureView>>( - future: _future, - onRetry: _load, - errorTitle: '运动计划加载失败', - onData: (ctx, plans) { - if (plans.isEmpty) return _empty(context, '运动计划', '暂无计划,点击右下角新建'); - return ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: plans.length, - itemBuilder: (ctx, i) { + future: _future, + onRetry: _load, + errorTitle: '运动计划加载失败', + onData: (ctx, plans) { + if (plans.isEmpty) return _empty(context, '运动计划', '暂无计划,点击右下角新建'); + final totalItems = plans.fold(0, (sum, p) { + final items = (p['items'] as List?) ?? []; + return sum + items.length; + }); + final doneItems = plans.fold(0, (sum, p) { + final items = + (p['items'] as List?)?.cast>() ?? []; + return sum + items.where((it) => it['isCompleted'] == true).length; + }); + return ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 88), + children: [ + EnterpriseHeader( + title: '运动计划', + subtitle: '按天追踪运动目标,保持稳定节奏', + icon: Icons.directions_run, + color: const Color(0xFF60A5FA), + accent: const Color(0xFF8B5CF6), + stats: [ + EnterpriseStat( + label: '计划数', + value: '${plans.length} 个', + icon: Icons.view_week_outlined, + ), + EnterpriseStat( + label: '总完成', + value: '$doneItems/$totalItems 天', + icon: Icons.check_circle_outline, + ), + ], + ), + const SizedBox(height: 10), + ...List.generate(plans.length, (i) { final p = plans[i]; final items = (p['items'] as List?)?.cast>() ?? []; @@ -747,8 +791,7 @@ class _ExercisePlanPageState extends ConsumerState { .length; final startDate = p['startDate']?.toString() ?? ''; final endDate = p['endDate']?.toString() ?? ''; - final now = DateTime.now(); - final todayKey = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; + final todayKey = _todayKey(); final todayItem = items.cast>().firstWhere( (it) => it['scheduledDate']?.toString() == todayKey, orElse: () => {}, @@ -762,61 +805,82 @@ class _ExercisePlanPageState extends ConsumerState { return SwipeDeleteTile( key: Key(p['id']?.toString() ?? '$i'), onDelete: () => _deletePlan(p['id']?.toString() ?? ''), - onTap: () {}, + margin: const EdgeInsets.symmetric(vertical: 4), + onTap: () => pushRoute( + ref, + 'exercisePlanDetail', + params: {'id': p['id']?.toString() ?? ''}, + ), child: Container( - padding: const EdgeInsets.all(16), + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(AppTheme.rMd), - border: Border.all(color: const Color(0xFFCFD6E0), width: 1.2), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), boxShadow: [AppTheme.shadowLight], ), child: Row( children: [ Container( - width: 44, - height: 44, + width: 36, + height: 36, decoration: BoxDecoration( - color: AppColors.iconBg, - borderRadius: BorderRadius.circular(AppTheme.rMd), + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.directions_run, - size: 25, - color: AppTheme.primary, + size: 21, + color: Color(0xFF60A5FA), ), ), - const SizedBox(width: 12), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - exerciseName, - style: const TextStyle( - fontSize: 19, - fontWeight: FontWeight.w600, - ), + Row( + children: [ + Expanded( + child: Text( + exerciseName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + ), + const SizedBox(width: 8), + Text( + '$done/$total 天', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xFF60A5FA), + ), + ), + ], ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( - '$startDate 至 $endDate · ${items.first['durationMinutes'] ?? 0}分钟/天', + '${_slashDate(startDate)} 至 ${_slashDate(endDate)} · ${items.first['durationMinutes'] ?? 0}分钟/天', + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 16, + fontSize: 13, color: AppColors.textSecondary, ), ), - const SizedBox(height: 6), - Text( - '$done/$total 天已完成', - style: const TextStyle( - fontSize: 15, - color: AppTheme.primary, - ), - ), ], ), ), + const SizedBox(width: 8), GestureDetector( onTap: hasTodayItem ? () => _checkIn( @@ -825,19 +889,19 @@ class _ExercisePlanPageState extends ConsumerState { ) : null, child: Container( - width: 40, - height: 40, + width: 34, + height: 34, decoration: BoxDecoration( color: todayDone ? AppColors.successLight : AppColors.cardInner, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(10), ), child: Icon( todayDone ? Icons.check_circle : Icons.check_circle_outline, - size: 28, + size: 24, color: todayDone ? AppTheme.success : AppColors.textHint, @@ -848,10 +912,425 @@ class _ExercisePlanPageState extends ConsumerState { ), ), ); - }, - ); - }, + }), + ], + ); + }, + ), + ); + } +} + +class ExercisePlanDetailPage extends ConsumerStatefulWidget { + final String id; + const ExercisePlanDetailPage({super.key, required this.id}); + + @override + ConsumerState createState() => + _ExercisePlanDetailPageState(); +} + +class _ExercisePlanDetailPageState + extends ConsumerState { + Future?>? _future; + + static const _exerciseColor = Color(0xFF60A5FA); + static const _exerciseDark = Color(0xFF7C5CFF); + static const _exerciseSoft = Color(0xFFEFF6FF); + @override + void initState() { + super.initState(); + _load(); + } + + void _load() { + setState(() { + _future = widget.id.isEmpty + ? Future?>.value(null) + : ref.read(exerciseServiceProvider).getPlanDetail(widget.id); + }); + } + + Future _checkIn(String itemId) async { + if (itemId.isEmpty) return; + await ref.read(exerciseServiceProvider).checkIn(itemId); + ref.invalidate(currentExercisePlanProvider); + _load(); + } + + String _todayKey() { + final now = DateTime.now(); + return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + return GradientScaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => popRoute(ref), ), + title: const Text('运动计划详情'), + ), + body: AppFutureView?>( + future: _future, + onRetry: _load, + errorTitle: '运动计划详情加载失败', + onData: (ctx, plan) { + if (plan == null) { + return _empty(context, '运动计划详情', '没有找到这条运动计划'); + } + + final items = + (plan['items'] as List?)?.cast>() ?? []; + final total = items.length; + final done = items.where((it) => it['isCompleted'] == true).length; + final progress = total == 0 ? 0.0 : done / total; + final today = _todayKey(); + final todayItem = items.firstWhere( + (it) => it['scheduledDate']?.toString() == today, + orElse: () => {}, + ); + final exerciseName = items.isNotEmpty + ? (items.first['exerciseType']?.toString() ?? '运动') + : '运动'; + final startDate = plan['startDate']?.toString() ?? ''; + final endDate = plan['endDate']?.toString() ?? ''; + + return RefreshIndicator( + onRefresh: () async => _load(), + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), + children: [ + _ExerciseHeroCard( + exerciseName: exerciseName, + startDate: startDate, + endDate: endDate, + durationMinutes: items.isNotEmpty + ? ((items.first['durationMinutes'] as num?)?.toInt() ?? 0) + : 0, + done: done, + total: total, + progress: progress, + ), + const SizedBox(height: 12), + _TodayExerciseCard( + item: todayItem, + onCheckIn: () => _checkIn(todayItem['id']?.toString() ?? ''), + ), + const SizedBox(height: 16), + const Text( + '每日安排', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 10), + if (items.isEmpty) + _empty(context, '每日安排', '这条计划还没有安排内容') + else + ...items.map( + (item) => _ExerciseDayTile( + date: item['scheduledDate']?.toString() ?? '', + name: item['exerciseType']?.toString() ?? '运动', + minutes: + ((item['durationMinutes'] as num?)?.toInt() ?? 0), + isCompleted: item['isCompleted'] == true, + isToday: item['scheduledDate']?.toString() == today, + onTap: () => _checkIn(item['id']?.toString() ?? ''), + ), + ), + ], + ), + ); + }, + ), + ); + } +} + +class _ExerciseHeroCard extends StatelessWidget { + final String exerciseName; + final String startDate; + final String endDate; + final int durationMinutes; + final int done; + final int total; + final double progress; + + const _ExerciseHeroCard({ + required this.exerciseName, + required this.startDate, + required this.endDate, + required this.durationMinutes, + required this.done, + required this.total, + required this.progress, + }); + + String _slashDate(String value) { + if (value.length >= 10) { + return value.substring(0, 10).replaceAll('-', '/'); + } + return value.replaceAll('-', '/'); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.directions_run, + color: Color(0xFF60A5FA), + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + exerciseName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: 20, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 4), + Text( + '${_slashDate(startDate)} 至 ${_slashDate(endDate)} · $durationMinutes分钟/天', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textSecondary, + fontSize: 13, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: const Color(0xFFBFDBFE)), + ), + child: Text( + '$done/$total 天', + style: const TextStyle( + color: Color(0xFF60A5FA), + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 14), + ClipRRect( + borderRadius: BorderRadius.circular(999), + child: LinearProgressIndicator( + minHeight: 8, + value: progress.clamp(0.0, 1.0), + backgroundColor: const Color(0xFFEFF6FF), + valueColor: const AlwaysStoppedAnimation( + Color(0xFF60A5FA), + ), + ), + ), + ], + ), + ); + } +} + +class _TodayExerciseCard extends StatelessWidget { + final Map item; + final VoidCallback onCheckIn; + + const _TodayExerciseCard({required this.item, required this.onCheckIn}); + + @override + Widget build(BuildContext context) { + final hasItem = item.isNotEmpty; + final isDone = item['isCompleted'] == true; + final name = item['exerciseType']?.toString() ?? '今日休息'; + final minutes = ((item['durationMinutes'] as num?)?.toInt() ?? 0); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], + ), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: isDone + ? AppColors.successLight + : _ExercisePlanDetailPageState._exerciseSoft, + borderRadius: BorderRadius.circular(11), + ), + child: Icon( + isDone ? Icons.check_circle : Icons.today_outlined, + color: isDone + ? AppTheme.success + : _ExercisePlanDetailPageState._exerciseColor, + size: 23, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + hasItem ? '今日任务' : '今日无任务', + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 3), + Text( + hasItem ? '$name · $minutes 分钟' : '可以好好恢复,保持轻量活动', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + ], + ), + ), + if (hasItem) + GestureDetector( + onTap: onCheckIn, + child: Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: isDone ? AppColors.successLight : AppColors.cardInner, + borderRadius: BorderRadius.circular(13), + ), + child: Icon( + isDone ? Icons.check_circle : Icons.check_circle_outline, + size: 28, + color: isDone ? AppTheme.success : AppColors.textHint, + ), + ), + ), + ], + ), + ); + } +} + +class _ExerciseDayTile extends StatelessWidget { + final String date; + final String name; + final int minutes; + final bool isCompleted; + final bool isToday; + final VoidCallback onTap; + + const _ExerciseDayTile({ + required this.date, + required this.name, + required this.minutes, + required this.isCompleted, + required this.isToday, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isToday + ? _ExercisePlanDetailPageState._exerciseColor + : AppColors.borderLight, + width: isToday ? 1.3 : 1, + ), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), + leading: Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: isCompleted + ? AppColors.successLight + : _ExercisePlanDetailPageState._exerciseSoft, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + isCompleted ? Icons.check : Icons.fitness_center, + color: isCompleted + ? AppTheme.success + : _ExercisePlanDetailPageState._exerciseDark, + size: 21, + ), + ), + title: Text( + isToday ? '$name · 今天' : name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + subtitle: Text( + '$date · $minutes 分钟', + style: const TextStyle(fontSize: 13, color: AppColors.textHint), + ), + trailing: IconButton( + onPressed: onTap, + icon: Icon( + isCompleted ? Icons.check_circle : Icons.check_circle_outline, + color: isCompleted ? AppTheme.success : AppColors.textHint, + ), + ), + ), ); } } @@ -897,7 +1376,8 @@ class _ExercisePlanCreatePageState '${_start.year}-${_start.month.toString().padLeft(2, '0')}-${_start.day.toString().padLeft(2, '0')}', 'endDate': '${end.year}-${end.month.toString().padLeft(2, '0')}-${end.day.toString().padLeft(2, '0')}', - 'reminderTime': '${_reminderTime.hour.toString().padLeft(2, '0')}:${_reminderTime.minute.toString().padLeft(2, '0')}', + 'reminderTime': + '${_reminderTime.hour.toString().padLeft(2, '0')}:${_reminderTime.minute.toString().padLeft(2, '0')}', }); ref.invalidate(currentExercisePlanProvider); if (mounted) { @@ -1028,21 +1508,33 @@ class _ExercisePlanCreatePageState } Widget _timeField() { - final value = '${_reminderTime.hour.toString().padLeft(2, '0')}:${_reminderTime.minute.toString().padLeft(2, '0')}'; + final value = + '${_reminderTime.hour.toString().padLeft(2, '0')}:${_reminderTime.minute.toString().padLeft(2, '0')}'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('每日提醒时间', style: TextStyle(fontSize: 17, color: AppColors.textSecondary)), + const Text( + '每日提醒时间', + style: TextStyle(fontSize: 17, color: AppColors.textSecondary), + ), const SizedBox(height: 6), GestureDetector( onTap: () async { - final picked = await showAppTimePicker(context, initialTime: _reminderTime); - if (picked != null && mounted) setState(() => _reminderTime = picked); + final picked = await showAppTimePicker( + context, + initialTime: _reminderTime, + ); + if (picked != null && mounted) { + setState(() => _reminderTime = picked); + } }, child: Container( width: double.infinity, padding: const EdgeInsets.all(14), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), child: Text(value, style: const TextStyle(fontSize: 19)), ), ), @@ -1214,7 +1706,7 @@ class _FollowUpItem extends StatelessWidget { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 10), Text( item['title']?.toString() ?? '', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600), @@ -1300,10 +1792,14 @@ class _HealthArchivePageState extends ConsumerState { _diagnosisCtrl.text = a['diagnosis'] ?? ''; final surgeries = a['surgeries'] as List?; if (surgeries != null && surgeries.isNotEmpty) { - _surgeries.addAll(surgeries.whereType().map((item) => { - 'type': item['type']?.toString() ?? '', - 'date': item['date']?.toString() ?? '', - })); + _surgeries.addAll( + surgeries.whereType().map( + (item) => { + 'type': item['type']?.toString() ?? '', + 'date': item['date']?.toString() ?? '', + }, + ), + ); } else { final st = a['surgeryType'] as String?; final sd = a['surgeryDate'] as String?; @@ -1352,10 +1848,14 @@ class _HealthArchivePageState extends ConsumerState { ); final surgeries = _surgeries .where((s) => (s['type'] ?? '').trim().isNotEmpty) - .map((s) => { - 'type': s['type']!.trim(), - 'date': (s['date'] ?? '').trim().isEmpty ? null : s['date']!.trim(), - }) + .map( + (s) => { + 'type': s['type']!.trim(), + 'date': (s['date'] ?? '').trim().isEmpty + ? null + : s['date']!.trim(), + }, + ) .toList(); final firstSurgery = surgeries.isEmpty ? null : surgeries.first; await srv.updateHealthArchive({ @@ -1395,8 +1895,7 @@ class _HealthArchivePageState extends ConsumerState { @override Widget build(BuildContext context) { if (_loading) { - return Scaffold( - backgroundColor: const Color(0xFFF6F8FC), + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1413,8 +1912,7 @@ class _HealthArchivePageState extends ConsumerState { body: const Center(child: CircularProgressIndicator()), ); } - return Scaffold( - backgroundColor: const Color(0xFFF6F8FC), + return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -1439,8 +1937,13 @@ class _HealthArchivePageState extends ConsumerState { ) : TextButton( onPressed: _save, - style: TextButton.styleFrom(foregroundColor: AppColors.textPrimary), - child: const Text('保存', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + style: TextButton.styleFrom( + foregroundColor: AppColors.textPrimary, + ), + child: const Text( + '保存', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), ), ], ), @@ -1495,7 +1998,7 @@ class _HealthArchivePageState extends ConsumerState { child: _DateF2( '日期', s['date'] ?? '', - () => _pickDate((v) => s['date'] = v), + () => _pickDate((v) => setState(() => s['date'] = v)), hint: '点击选择', ), ), @@ -1542,20 +2045,20 @@ class _Section extends StatelessWidget { final IconData icon; final List fields; const _Section(this.title, this.icon, this.fields); + Color get _color => switch (title) { + '个人资料' => const Color(0xFF2563EB), + '医疗信息' => const Color(0xFF7C5CFF), + '病史与限制' => const Color(0xFFF59E0B), + _ => AppColors.primary, + }; @override Widget build(BuildContext c) => Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFFD5DCE5)), - boxShadow: [ - BoxShadow( - color: const Color(0xFF101828).withValues(alpha: 0.08), - blurRadius: 18, - offset: const Offset(0, 6), - ), - ], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1566,10 +2069,10 @@ class _Section extends StatelessWidget { width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.iconBg, + color: _color.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(10), ), - child: Icon(icon, size: 20, color: AppColors.primary), + child: Icon(icon, size: 20, color: _color), ), const SizedBox(width: 10), Text( @@ -1610,22 +2113,22 @@ class _F extends StatelessWidget { hintText: hint, hintStyle: const TextStyle(fontSize: 14, color: AppColors.textHint), filled: true, - fillColor: Colors.white, + fillColor: const Color(0xFFF8FAFC), contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 12, ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.borderLight), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.borderLight), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.primary), + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF60A5FA)), ), ), ), @@ -1656,22 +2159,22 @@ class _F2 extends StatelessWidget { hintText: hint, hintStyle: const TextStyle(fontSize: 13, color: AppColors.textHint), filled: true, - fillColor: Colors.white, + fillColor: const Color(0xFFF8FAFC), contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: AppColors.border), + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.borderLight), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: AppColors.border), + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.borderLight), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: AppColors.primary), + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFF60A5FA)), ), ), ), @@ -1699,9 +2202,9 @@ class _DateF extends StatelessWidget { height: 48, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.border), + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), ), alignment: Alignment.centerLeft, child: Row( @@ -1717,9 +2220,9 @@ class _DateF extends StatelessWidget { ), const Spacer(), const Icon( - Icons.calendar_today, + Icons.calendar_today_outlined, size: 18, - color: AppColors.textHint, + color: Color(0xFF7C5CFF), ), ], ), @@ -1750,9 +2253,9 @@ class _DateF2 extends StatelessWidget { height: 44, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColors.border), + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.borderLight), ), alignment: Alignment.centerLeft, child: Row( @@ -1768,9 +2271,9 @@ class _DateF2 extends StatelessWidget { ), const Spacer(), const Icon( - Icons.calendar_today, + Icons.calendar_today_outlined, size: 14, - color: AppColors.textHint, + color: Color(0xFF7C5CFF), ), ], ), @@ -1790,17 +2293,18 @@ class _AddBtn extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: AppColors.primarySoft, - borderRadius: BorderRadius.circular(8), + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFBFDBFE)), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.add, size: 14, color: AppColors.primary), + Icon(Icons.add, size: 14, color: Color(0xFF7C5CFF)), SizedBox(width: 2), Text( '添加', - style: TextStyle(fontSize: 13, color: AppColors.primary), + style: TextStyle(fontSize: 13, color: Color(0xFF7C5CFF)), ), ], ), @@ -1823,8 +2327,9 @@ class _HealthCalendarPageState extends ConsumerState { Map? _selectedDayData; static const _medBlue = Color(0xFF3B82F6); - static const _exGreen = Color(0xFF22C55E); + static const _exGreen = Color(0xFF60A5FA); static const _fupOrange = Color(0xFFF59E0B); + static const _healthBlue = Color(0xFF2563EB); @override void initState() { @@ -1907,14 +2412,24 @@ class _HealthCalendarPageState extends ConsumerState { ), body: Column( children: [ - Container( - color: Colors.white, - child: Column( - children: [ - _buildMonthHeader(), - _buildWeekdayHeader(), - _buildCalendarGrid(), - ], + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Container( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], + ), + child: Column( + children: [ + _buildMonthHeader(), + _buildWeekdayHeader(), + const SizedBox(height: 6), + _buildCalendarGrid(), + ], + ), ), ), const SizedBox(height: 12), @@ -1925,9 +2440,9 @@ class _HealthCalendarPageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ _Legend('用药提醒', _medBlue), - const SizedBox(width: 20), + const SizedBox(width: 10), _Legend('运动计划', _exGreen), - const SizedBox(width: 20), + const SizedBox(width: 10), _Legend('复查随访', _fupOrange), ], ), @@ -1947,6 +2462,10 @@ class _HealthCalendarPageState extends ConsumerState { children: [ IconButton( icon: const Icon(Icons.chevron_left), + style: IconButton.styleFrom( + backgroundColor: const Color(0xFFF8FAFC), + foregroundColor: _healthBlue, + ), onPressed: () { setState( () => _currentMonth = DateTime( @@ -1963,6 +2482,10 @@ class _HealthCalendarPageState extends ConsumerState { ), IconButton( icon: const Icon(Icons.chevron_right), + style: IconButton.styleFrom( + backgroundColor: const Color(0xFFF8FAFC), + foregroundColor: _healthBlue, + ), onPressed: () { setState( () => _currentMonth = DateTime( @@ -2031,14 +2554,16 @@ class _HealthCalendarPageState extends ConsumerState { margin: const EdgeInsets.all(2), decoration: BoxDecoration( color: isSel - ? AppColors.primarySoft - : (isToday ? AppColors.infoLight : Colors.white), + ? _healthBlue + : (isToday + ? _healthBlue.withValues(alpha: 0.08) + : Colors.white), borderRadius: BorderRadius.circular(12), border: isSel - ? Border.all(color: AppColors.primary, width: 1.5) + ? Border.all(color: _healthBlue, width: 1.5) : (isToday ? Border.all( - color: AppColors.primary.withValues(alpha: 0.3), + color: _healthBlue.withValues(alpha: 0.24), width: 1, ) : null), @@ -2050,7 +2575,7 @@ class _HealthCalendarPageState extends ConsumerState { '$day', style: TextStyle( fontSize: 16, - color: isSel ? AppColors.primary : AppColors.textPrimary, + color: isSel ? Colors.white : AppColors.textPrimary, fontWeight: isSel || isToday ? FontWeight.w600 : FontWeight.normal, @@ -2209,21 +2734,21 @@ class _SectionTitle extends StatelessWidget { child: Row( children: [ Container( - width: 4, - height: 16, + width: 6, + height: 20, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), - Icon(icon, size: 16, color: color), + Icon(icon, size: 18, color: color), const SizedBox(width: 6), Text( title, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, + fontSize: 15, + fontWeight: FontWeight.w800, color: color, ), ), @@ -2240,25 +2765,29 @@ class _EventCard extends StatelessWidget { @override Widget build(BuildContext c) => Container( margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], ), child: Row( children: [ Container( - width: 8, - height: 8, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), + width: 34, + height: 34, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.event_available, size: 18, color: color), ), const SizedBox(width: 10), Expanded( child: Text( title, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), ), ), Text( @@ -2278,14 +2807,30 @@ class _Legend extends StatelessWidget { Widget build(BuildContext c) => Row( children: [ Container( - width: 8, - height: 8, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ), - const SizedBox(width: 4), - Text( - label, - style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.12)), + ), + child: Row( + children: [ + Container( + width: 7, + height: 7, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: 12, + color: color, + fontWeight: FontWeight.w700, + ), + ), + ], + ), ), ], ); @@ -2392,11 +2937,34 @@ class StaticTextPage extends ConsumerWidget { data: contents[type] ?? '内容加载中...', selectable: true, styleSheet: MarkdownStyleSheet( - p: const TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary), - h1: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.5), - h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.5), - h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.textPrimary, height: 1.5), - listBullet: const TextStyle(fontSize: 17, height: 1.8, color: AppColors.textPrimary), + p: const TextStyle( + fontSize: 17, + height: 1.8, + color: AppColors.textPrimary, + ), + h1: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + height: 1.5, + ), + h2: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + height: 1.5, + ), + h3: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + height: 1.5, + ), + listBullet: const TextStyle( + fontSize: 17, + height: 1.8, + color: AppColors.textPrimary, + ), ), ), ), diff --git a/health_app/lib/pages/report/report_pages.dart b/health_app/lib/pages/report/report_pages.dart index 90e0eeb..f3cd818 100644 --- a/health_app/lib/pages/report/report_pages.dart +++ b/health_app/lib/pages/report/report_pages.dart @@ -10,6 +10,7 @@ import '../../core/app_theme.dart'; import '../../core/api_client.dart' show baseUrl; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; +import '../../widgets/enterprise_widgets.dart'; final reportProvider = NotifierProvider( ReportNotifier.new, @@ -321,13 +322,20 @@ class ReportNotifier extends Notifier { Future reanalyzeReport(String id) async { try { - final res = await ref.read(apiClientProvider).post('/api/reports/$id/reanalyze'); + final res = await ref + .read(apiClientProvider) + .post('/api/reports/$id/reanalyze'); final data = res.data; if (data is Map && data['code'] != 0) { - state = state.copyWith(uploadError: data['message']?.toString() ?? '重新分析失败'); + state = state.copyWith( + uploadError: data['message']?.toString() ?? '重新分析失败', + ); return; } - state = state.copyWith(clearCurrentAnalysis: true, clearUploadError: true); + state = state.copyWith( + clearCurrentAnalysis: true, + clearUploadError: true, + ); await loadReports(); fetchReportDetail(id); } catch (e) { @@ -345,6 +353,9 @@ class ReportNotifier extends Notifier { class ReportListPage extends ConsumerWidget { const ReportListPage({super.key}); + static const _reportBlue = Color(0xFF8B5CF6); + static const _reportCyan = Color(0xFF38BDF8); + @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(reportProvider); @@ -352,14 +363,17 @@ class ReportListPage extends ConsumerWidget { if (state.isAnalyzing) { return GradientScaffold( appBar: AppBar( - leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref)), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => popRoute(ref), + ), title: const Text('报告管理'), ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator(color: AppColors.primary), + const CircularProgressIndicator(color: _reportBlue), const SizedBox(height: 16), Text( state.uploadingImage == null ? 'AI 正在分析报告...' : '正在上传报告...', @@ -389,6 +403,27 @@ class ReportListPage extends ConsumerWidget { child: ListView( padding: const EdgeInsets.all(16), children: [ + EnterpriseHeader( + title: '报告管理', + subtitle: '上传检查报告后自动进行 AI 结构化解读', + icon: Icons.description_outlined, + color: _reportBlue, + accent: _reportCyan, + stats: [ + EnterpriseStat( + label: '报告总数', + value: '${state.reports.length} 份', + icon: Icons.folder_copy_outlined, + ), + EnterpriseStat( + label: '待处理', + value: + '${state.reports.where((r) => r.aiStatus == 'Analyzing' || r.reviewStatus != 'Reviewed').length} 份', + icon: Icons.pending_actions_outlined, + ), + ], + ), + const SizedBox(height: 10), if (state.uploadError != null) ...[ _buildUploadError(state.uploadError!), const SizedBox(height: 12), @@ -435,7 +470,7 @@ class ReportListPage extends ConsumerWidget { Widget _buildUploadButton(BuildContext context, WidgetRef ref) { return FloatingActionButton( onPressed: () => _showUploadOptions(context, ref), - backgroundColor: AppColors.primary, + backgroundColor: _reportBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: const Icon(Icons.add, size: 28, color: Colors.white), ); @@ -456,7 +491,7 @@ class ReportListPage extends ConsumerWidget { ListTile( leading: const Icon( Icons.camera_alt_outlined, - color: AppColors.primary, + color: _reportBlue, ), title: const Text('拍照上传', style: TextStyle(fontSize: 17)), onTap: () async { @@ -474,7 +509,7 @@ class ReportListPage extends ConsumerWidget { ListTile( leading: const Icon( Icons.photo_library_outlined, - color: AppColors.primary, + color: _reportCyan, ), title: const Text('从相册选择', style: TextStyle(fontSize: 17)), onTap: () async { @@ -505,13 +540,13 @@ class ReportListPage extends ConsumerWidget { width: 100, height: 100, decoration: BoxDecoration( - color: AppColors.cardInner, + color: _reportBlue.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(50), ), child: const Icon( Icons.description_outlined, size: 44, - color: AppColors.textHint, + color: _reportBlue, ), ), const SizedBox(height: 20), @@ -545,38 +580,34 @@ class ReportListPage extends ConsumerWidget { margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: const Color(0xFFD5DCE5)), - boxShadow: [ - BoxShadow( - color: const Color(0xFF101828).withValues(alpha: 0.08), - blurRadius: 18, - offset: const Offset(0, 6), - ), - ], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], ), child: Material( color: Colors.transparent, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(16), child: InkWell( onTap: () => pushRoute(ref, 'aiAnalysis', params: {'id': report.id}), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(16), child: Padding( - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(15), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( - color: AppColors.iconBg, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.borderLight), + color: _reportBlue.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: _reportBlue.withValues(alpha: 0.10), + ), ), child: const Icon( Icons.description_outlined, size: 26, - color: AppColors.primary, + color: _reportBlue, ), ), const SizedBox(width: 14), @@ -586,9 +617,11 @@ class ReportListPage extends ConsumerWidget { children: [ Text( displayTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 18, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), @@ -619,15 +652,13 @@ class ReportListPage extends ConsumerWidget { ), _buildStatusBadge(report), const SizedBox(width: 4), - GestureDetector( - onTap: () => _confirmDelete(context, ref, report.id), - child: const Padding( - padding: EdgeInsets.all(6), - child: Icon( - Icons.delete_outline, - size: 20, - color: AppColors.textHint, - ), + IconButton( + onPressed: () => _confirmDelete(context, ref, report.id), + visualDensity: VisualDensity.compact, + icon: const Icon( + Icons.delete_outline, + size: 20, + color: AppColors.textHint, ), ), ], @@ -663,26 +694,39 @@ class ReportListPage extends ConsumerWidget { Widget _buildStatusBadge(ReportItem report) { final (label, bg, fg) = switch (report.status) { - _ when report.reviewStatus == 'Reviewed' => ('已审核', AppColors.successLight, AppColors.success), - _ when report.aiStatus == 'Analyzing' => ('分析中', AppColors.cardInner, AppColors.textHint), - _ when report.aiStatus == 'Failed' => ('分析失败', AppColors.error.withValues(alpha: 0.08), AppColors.error), - _ when report.aiStatus == 'Succeeded' => ('待审核', AppColors.warningLight, AppColors.warning), - _ => ('分析中', AppColors.cardInner, AppColors.textHint), + _ when report.reviewStatus == 'Reviewed' => ( + '已审核', + AppColors.successLight, + AppColors.success, + ), + _ when report.aiStatus == 'Analyzing' => ( + '分析中', + _reportBlue.withValues(alpha: 0.08), + _reportBlue, + ), + _ when report.aiStatus == 'Failed' => ( + '分析失败', + AppColors.error.withValues(alpha: 0.08), + AppColors.error, + ), + _ when report.aiStatus == 'Succeeded' => ( + '待审核', + AppColors.warningLight, + AppColors.warning, + ), + _ => ('分析中', _reportBlue.withValues(alpha: 0.08), _reportBlue), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: bg, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: fg.withValues(alpha: 0.12)), ), child: Text( label, - style: TextStyle( - fontSize: 13, - color: fg, - fontWeight: FontWeight.w500, - ), + style: TextStyle(fontSize: 13, color: fg, fontWeight: FontWeight.w700), ), ); } @@ -719,18 +763,17 @@ class ReportOriginalPage extends StatelessWidget { final String url; final String title; - const ReportOriginalPage({ - super.key, - required this.url, - this.title = '原始报告', - }); + const ReportOriginalPage({super.key, required this.url, this.title = '原始报告'}); @override Widget build(BuildContext context) { final imageUrl = _absoluteUrl(url); return GradientScaffold( appBar: AppBar( - leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context)), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), title: Text(title), ), body: Center( diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index 48efea0..7fa3c37 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -55,21 +55,6 @@ class SettingsPage extends ConsumerWidget { onTap: () => pushRoute(ref, 'notificationPrefs'), ), const SizedBox(height: 8), - _SettingsTile( - icon: LucideIcons.type, - title: '字体大小', - subtitle: '老年友好', - colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)], - onTap: () {}, - ), - const SizedBox(height: 8), - _SettingsTile( - icon: LucideIcons.sprayCan, - title: '清除缓存', - colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)], - onTap: () {}, - ), - const SizedBox(height: 8), _SettingsTile( icon: LucideIcons.info, title: '关于小脉健康', diff --git a/health_app/lib/widgets/enterprise_widgets.dart b/health_app/lib/widgets/enterprise_widgets.dart new file mode 100644 index 0000000..30603cc --- /dev/null +++ b/health_app/lib/widgets/enterprise_widgets.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; +import '../core/app_colors.dart'; +import '../core/app_theme.dart'; + +class EnterpriseStat { + final String label; + final String value; + final IconData? icon; + + const EnterpriseStat({required this.label, required this.value, this.icon}); +} + +class EnterpriseHeader extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Color color; + final Color accent; + final List stats; + final Widget? trailing; + + const EnterpriseHeader({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.color, + required this.accent, + this.stats = const [], + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + for (var i = 0; i < stats.length; i++) ...[ + Expanded( + child: _HeaderStat( + stat: stats[i], + color: i.isEven ? color : accent, + accent: i.isEven ? accent : color, + ), + ), + if (i != stats.length - 1) const SizedBox(width: 8), + ], + ], + ); + } +} + +class _HeaderStat extends StatelessWidget { + final EnterpriseStat stat; + final Color color; + final Color accent; + + const _HeaderStat({ + required this.stat, + required this.color, + required this.accent, + }); + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(minHeight: 54), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + color.withValues(alpha: 0.10), + accent.withValues(alpha: 0.06), + ], + ), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.borderLight, width: 1.1), + ), + child: Row( + children: [ + if (stat.icon != null) ...[ + Icon(stat.icon, color: color, size: 16), + const SizedBox(width: 6), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stat.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + stat.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textSecondary, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class EnterpriseSectionCard extends StatelessWidget { + final String? title; + final IconData? icon; + final Color color; + final Widget child; + final EdgeInsetsGeometry padding; + final Widget? trailing; + + const EnterpriseSectionCard({ + super.key, + this.title, + this.icon, + required this.color, + required this.child, + this.trailing, + this.padding = const EdgeInsets.all(16), + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [AppTheme.shadowLight], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) ...[ + Row( + children: [ + if (icon != null) ...[ + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 19, color: color), + ), + const SizedBox(width: 10), + ], + Expanded( + child: Text( + title!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + ), + ?trailing, + ], + ), + const SizedBox(height: 14), + ], + child, + ], + ), + ); + } +} + +class EnterpriseFab extends StatelessWidget { + final VoidCallback onPressed; + final IconData icon; + final Color color; + + const EnterpriseFab({ + super.key, + required this.onPressed, + required this.icon, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + onPressed: onPressed, + backgroundColor: color, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Icon(icon, size: 26, color: Colors.white), + ); + } +}