diff --git a/PC b/PC new file mode 100644 index 0000000..7a8c905 --- /dev/null +++ b/PC @@ -0,0 +1 @@ + Phone - forwarding OK diff --git a/backend/src/Health.WebApi/Endpoints/user_endpoints.cs b/backend/src/Health.WebApi/Endpoints/user_endpoints.cs index 9be6192..8f76a6c 100644 --- a/backend/src/Health.WebApi/Endpoints/user_endpoints.cs +++ b/backend/src/Health.WebApi/Endpoints/user_endpoints.cs @@ -73,8 +73,33 @@ public static class UserEndpoints group.MapDelete("/account", async (HttpContext http, AppDbContext db, CancellationToken ct) => { var userId = GetUserId(http); - var user = await db.Users.FindAsync([userId], ct); - if (user != null) { db.Users.Remove(user); await db.SaveChangesAsync(ct); } + // 医生:清除Doctor关联 + var profile = await db.DoctorProfiles.FirstOrDefaultAsync(p => p.UserId == userId, ct); + if (profile?.DoctorId != null) + { + await db.Users.Where(u => u.DoctorId == profile!.DoctorId).ExecuteUpdateAsync(u => u.SetProperty(x => x.DoctorId, (Guid?)null), ct); + await db.Doctors.Where(d => d.Id == profile.DoctorId).ExecuteDeleteAsync(ct); + } + // 删除所有关联数据 + await db.HealthRecords.Where(r => r.UserId == userId).ExecuteDeleteAsync(ct); + await db.MedicationLogs.Where(l => l.UserId == userId).ExecuteDeleteAsync(ct); + await db.Medications.Where(m => m.UserId == userId).ExecuteDeleteAsync(ct); + await db.DietFoodItems.Where(i => i.DietRecord!.UserId == userId).ExecuteDeleteAsync(ct); + await db.DietRecords.Where(d => d.UserId == userId).ExecuteDeleteAsync(ct); + await db.ExercisePlanItems.Where(i => i.Plan!.UserId == userId).ExecuteDeleteAsync(ct); + await db.ExercisePlans.Where(p => p.UserId == userId).ExecuteDeleteAsync(ct); + await db.Reports.Where(r => r.UserId == userId).ExecuteDeleteAsync(ct); + await db.ConversationMessages.Where(m => m.Conversation!.UserId == userId).ExecuteDeleteAsync(ct); + await db.Conversations.Where(c => c.UserId == userId).ExecuteDeleteAsync(ct); + await db.ConsultationMessages.Where(m => m.Consultation!.UserId == userId).ExecuteDeleteAsync(ct); + await db.Consultations.Where(c => c.UserId == userId).ExecuteDeleteAsync(ct); + await db.FollowUps.Where(f => f.UserId == userId).ExecuteDeleteAsync(ct); + await db.RefreshTokens.Where(t => t.UserId == userId).ExecuteDeleteAsync(ct); + await db.DeviceTokens.Where(t => t.UserId == userId).ExecuteDeleteAsync(ct); + await db.NotificationPreferences.Where(p => p.UserId == userId).ExecuteDeleteAsync(ct); + await db.HealthArchives.Where(a => a.UserId == userId).ExecuteDeleteAsync(ct); + await db.DoctorProfiles.Where(p => p.UserId == userId).ExecuteDeleteAsync(ct); + await db.Users.Where(u => u.Id == userId).ExecuteDeleteAsync(ct); return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null }); }); } diff --git a/health_app/lib/core/app_router.dart b/health_app/lib/core/app_router.dart index 877d118..0f9b234 100644 --- a/health_app/lib/core/app_router.dart +++ b/health_app/lib/core/app_router.dart @@ -13,7 +13,6 @@ import '../pages/consultation/consultation_pages.dart'; import '../pages/settings/settings_pages.dart'; import '../pages/settings/notification_prefs_page.dart'; import '../pages/profile/profile_page.dart'; -import '../pages/profile/service_package_detail_page.dart'; import '../pages/diet/diet_capture_page.dart'; import '../pages/device/device_scan_page.dart'; import '../pages/device/device_management_page.dart'; @@ -32,45 +31,74 @@ import '../providers/auth_provider.dart' show userRoleProvider; Widget buildPage(RouteInfo route, WidgetRef ref) { final params = route.params; switch (route.name) { - case 'login': return const LoginPage(); + case 'login': + return const LoginPage(); case 'home': final role = ref.watch(userRoleProvider); return role == 'Doctor' ? const DoctorHomePage() : const HomePage(); - case 'doctorHome': return const DoctorHomePage(); - case 'adminHome': return const AdminHomePage(); - case 'adminAddDoctor': return const AdminAddDoctorPage(); - case 'trend': return TrendPage(metricType: params['type']?.isNotEmpty == true ? params['type'] : null); - case 'calendar': return const HealthCalendarPage(); - case 'medications': return const MedicationListPage(); - case 'medCheckIn': return MedicationCheckInPage(medId: params['id']); - case 'medicationEdit': return const MedicationEditPage(); - case 'reports': return const ReportListPage(); - case 'aiAnalysis': return AiAnalysisPage(id: params['id']!); - case 'consultation': return DoctorChatPage(id: params['id']!); - case 'exercisePlan': return const ExercisePlanPage(); - case 'exerciseCreate': return const ExercisePlanCreatePage(); - case 'dietRecords': return const DietRecordListPage(); - case 'dietCapture': return const DietCapturePage(); - case 'profile': return const ProfilePage(); - case 'doctorProfile': return const DoctorProfileEditPage(); - case 'doctorSettings': return const DoctorSettingsPage(); - case 'doctorPatientDetail': return DoctorPatientDetailPage(id: params['id']!); - case 'doctorChat': return DoctorChatPage(id: params['id']!); - case 'doctorReportDetail': return DoctorReportDetailPage(id: params['id']!); - case 'doctorFollowUpEdit': return DoctorFollowUpEditPage(id: params['id']!); - case 'devices': return const DeviceManagementPage(); - case 'deviceScan': return const DeviceScanPage(); - case 'healthArchive': return const HealthArchivePage(); - case 'followups': return const FollowUpListPage(); - case 'settings': return const SettingsPage(); - case 'notificationPrefs': return const NotificationPrefsPage(); - case 'staticText': return StaticTextPage(type: params['type']!); - case 'servicePackageDetail': return ServicePackageDetailPage(packageId: params['id']!); - case 'dietDetail': return DietRecordDetailPage(id: params['id']!); - default: return const LoginPage(); + case 'doctorHome': + return const DoctorHomePage(); + case 'adminHome': + return const AdminHomePage(); + case 'adminAddDoctor': + return const AdminAddDoctorPage(); + case 'trend': + return TrendPage( + metricType: params['type']?.isNotEmpty == true ? params['type'] : null, + ); + case 'calendar': + return const HealthCalendarPage(); + case 'medications': + return const MedicationListPage(); + case 'medCheckIn': + return MedicationCheckInPage(medId: params['id']); + case 'medicationEdit': + return const MedicationEditPage(); + case 'reports': + return const ReportListPage(); + case 'aiAnalysis': + return AiAnalysisPage(id: params['id']!); + case 'exercisePlan': + return const ExercisePlanPage(); + case 'exerciseCreate': + return const ExercisePlanCreatePage(); + case 'dietRecords': + return const DietRecordListPage(); + case 'dietCapture': + return const DietCapturePage(); + case 'profile': + return const ProfilePage(); + case 'doctorProfile': + return const DoctorProfileEditPage(); + case 'doctorSettings': + return const DoctorSettingsPage(); + case 'doctorPatientDetail': + return DoctorPatientDetailPage(id: params['id']!); + case 'doctorChat': + return DoctorChatPage(id: params['id']!); + case 'doctorReportDetail': + return DoctorReportDetailPage(id: params['id']!); + case 'doctorFollowUpEdit': + return DoctorFollowUpEditPage(id: params['id']!); + case 'devices': + return const DeviceManagementPage(); + case 'deviceScan': + return const DeviceScanPage(); + case 'healthArchive': + return const HealthArchivePage(); + case 'followups': + return const FollowUpListPage(); + case 'settings': + return const SettingsPage(); + case 'notificationPrefs': + return const NotificationPrefsPage(); + case 'staticText': + return StaticTextPage(type: params['type']!); + case 'dietDetail': + return DietRecordDetailPage(id: params['id']!); + default: + return const LoginPage(); } } // ── 医生端次级页面 Stubs(逐步实现)── - - diff --git a/health_app/lib/pages/diet/diet_capture_page.dart b/health_app/lib/pages/diet/diet_capture_page.dart index cd7ddfb..5d861a3 100644 --- a/health_app/lib/pages/diet/diet_capture_page.dart +++ b/health_app/lib/pages/diet/diet_capture_page.dart @@ -302,8 +302,14 @@ class DietNotifier extends Notifier { } // ─────────── 饮食主题色(暖橙系,不再用紫色)─────────── -const _dietAccent = AppColors.primary; -const _dietAccentLight = AppColors.primarySoft; +const _dietAccent = Color(0xFFF97316); +const _dietAccentLight = Color(0xFFFFF3E0); +const _dietKcalText = Color(0xFF9A3412); +const _dietGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFF6D365), Color(0xFFFDA085)], +); class DietCapturePage extends ConsumerStatefulWidget { const DietCapturePage({super.key}); @@ -404,11 +410,11 @@ class _DietCapturePageState extends ConsumerState { ('🍪', '加餐', 'snack'), ]; return Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - gradient: AppColors.surfaceGradient, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.borderLight), + color: Colors.white.withValues(alpha: 0.92), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white, width: 1.4), boxShadow: AppColors.cardShadowLight, ), child: Row( @@ -419,23 +425,28 @@ class _DietCapturePageState extends ConsumerState { onTap: () => ref.read(dietProvider.notifier).setMealType(m.$3), child: AnimatedContainer( duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(vertical: 12), + curve: Curves.easeOutCubic, + margin: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( - gradient: sel ? AppColors.warmCareGradient : null, - color: sel ? null : Colors.white, - borderRadius: BorderRadius.circular(12), + gradient: sel ? _dietGradient : null, + color: sel ? null : const Color(0xFFFFFBF6), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: sel ? Colors.transparent : const Color(0xFFFFE4CA), + ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(m.$1, style: const TextStyle(fontSize: 22)), - const SizedBox(height: 2), + const SizedBox.shrink(), + const SizedBox(height: 0), Text( m.$2, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: sel ? Colors.white : AppColors.textHint, + fontSize: 15, + fontWeight: FontWeight.w900, + color: sel ? Colors.white : _dietKcalText, ), ), ], @@ -523,7 +534,7 @@ class _DietCapturePageState extends ConsumerState { vertical: 4, ), decoration: BoxDecoration( - color: AppColors.warningLight, + color: const Color(0xFFFFF3D8), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -531,7 +542,7 @@ class _DietCapturePageState extends ConsumerState { style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.warning, + color: _dietKcalText, ), ), ), @@ -645,7 +656,7 @@ class _DietCapturePageState extends ConsumerState { style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.warning, + color: _dietKcalText, ), align: TextAlign.right, keyboardType: TextInputType.number, @@ -656,7 +667,7 @@ class _DietCapturePageState extends ConsumerState { 'kcal', style: TextStyle( fontSize: 13, - color: AppColors.textHint, + color: Color(0xFFB45309), ), ), ], @@ -715,7 +726,7 @@ class _DietCapturePageState extends ConsumerState { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide(color: AppColors.primary), + borderSide: const BorderSide(color: _dietAccent), ), hintText: hint, hintStyle: const TextStyle(fontSize: 14, color: AppColors.textHint), @@ -737,62 +748,62 @@ class _DietCapturePageState extends ConsumerState { border: Border.all(color: AppColors.border), boxShadow: AppColors.cardShadowLight, ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 56, - height: 56, - child: Stack( - alignment: Alignment.center, - children: [ - SizedBox( - width: 56, - height: 56, - child: CircularProgressIndicator( - value: (totalCal / 700).clamp(0.0, 1.0), - strokeWidth: 4, - backgroundColor: AppColors.borderLight, - color: _dietAccent, - ), - ), - Column( - mainAxisSize: MainAxisSize.min, + const Text( + '本餐热量', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 14), + Row( + children: [ + SizedBox( + width: 56, + height: 56, + child: Stack( + alignment: Alignment.center, children: [ - Text( - '$totalCal', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, + SizedBox( + width: 56, + height: 56, + child: CircularProgressIndicator( + value: (totalCal / 700).clamp(0.0, 1.0), + strokeWidth: 4, + backgroundColor: AppColors.borderLight, color: _dietAccent, ), ), - const Text( - 'kcal', - style: TextStyle( - fontSize: 12, - color: AppColors.textSecondary, - ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$totalCal', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: _dietAccent, + ), + ), + const Text( + 'kcal', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], ), ], ), - ], - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '本餐热量', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 6), - Row( + ), + const SizedBox(width: 16), + Expanded( + child: Row( children: [ _macro('碳水', 0.55, const Color(0xFFF5A623)), const SizedBox(width: 8), @@ -801,8 +812,8 @@ class _DietCapturePageState extends ConsumerState { _macro('脂肪', 0.20, const Color(0xFFE8686A)), ], ), - ], - ), + ), + ], ), ], ), @@ -855,28 +866,46 @@ class _DietCapturePageState extends ConsumerState { borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), ), - child: Row( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: _dietAccentLight, - borderRadius: BorderRadius.circular(10), + const Text( + 'AI建议', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, ), - child: const Icon(Icons.auto_awesome, size: 20, color: _dietAccent), ), - const SizedBox(width: 12), - Expanded( - child: Text( - text, - style: const TextStyle( - fontSize: 16, - color: AppColors.textPrimary, - height: 1.6, + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: _dietAccentLight, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.auto_awesome, + size: 20, + color: _dietAccent, + ), ), - ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: const TextStyle( + fontSize: 16, + color: AppColors.textPrimary, + height: 1.6, + ), + ), + ), + ], ), ], ), @@ -889,9 +918,15 @@ class _DietCapturePageState extends ConsumerState { width: double.infinity, height: 52, decoration: BoxDecoration( - gradient: AppColors.warmCareGradient, + gradient: _dietGradient, borderRadius: BorderRadius.circular(16), - boxShadow: AppColors.buttonShadow, + boxShadow: [ + BoxShadow( + color: _dietAccent.withValues(alpha: 0.24), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], ), child: Material( color: Colors.transparent, diff --git a/health_app/lib/pages/home/home_page.dart b/health_app/lib/pages/home/home_page.dart index 29db7fd..2dd5e77 100644 --- a/health_app/lib/pages/home/home_page.dart +++ b/health_app/lib/pages/home/home_page.dart @@ -176,7 +176,7 @@ class _HomePageState extends ConsumerState { } static final _agentDefs = [ - (ActiveAgent.consultation, '问诊', LucideIcons.messageCircle), + (ActiveAgent.consultation, 'AI问诊', LucideIcons.messageCircle), (ActiveAgent.health, '记数据', LucideIcons.heartPulse), (ActiveAgent.diet, '拍饮食', LucideIcons.utensils), (ActiveAgent.medication, '药管家', LucideIcons.pill), @@ -244,12 +244,36 @@ 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, + ActiveAgent.consultation => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFFCAD4), Color(0xFFFF9AAE)], + ), + ActiveAgent.health => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)], + ), + ActiveAgent.diet => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFF6D365), Color(0xFFFDA085)], + ), + ActiveAgent.medication => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)], + ), + ActiveAgent.report => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)], + ), + ActiveAgent.exercise => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFB6F2FF), Color(0xFF7DD3FC)], + ), _ => AppColors.primaryGradient, }; } 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 f5967e6..2d70066 100644 --- a/health_app/lib/pages/home/widgets/chat_messages_view.dart +++ b/health_app/lib/pages/home/widgets/chat_messages_view.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../core/app_colors.dart'; import '../../../core/app_theme.dart'; import '../../../core/navigation_provider.dart'; @@ -294,12 +295,27 @@ class ChatMessagesView extends ConsumerWidget { fit: BoxFit.cover, ), - // ── 医生选择区(问诊专用)── + // ── AI问诊引导 ── if (agent == ActiveAgent.consultation) ...[ const SizedBox(height: 14), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: _buildDoctorCards(ref, agentColors), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF0F4FF), + borderRadius: BorderRadius.circular(14), + ), + child: const Text( + '在下方输入框描述您的症状,AI医生将为您提供专业建议', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + ), + ), + ), ), ], @@ -409,13 +425,18 @@ class ChatMessagesView extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ ShaderMask( - shaderCallback: (bounds) => AppColors.actionOutlineGradient.createShader(bounds), + shaderCallback: (bounds) => + AppColors.actionOutlineGradient.createShader(bounds), child: Icon(a.icon, size: 24, color: Colors.white), ), const SizedBox(width: 10), Text( a.label, - style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + ), ), ], ), @@ -436,113 +457,6 @@ class ChatMessagesView extends ConsumerWidget { }; } - Widget _buildDoctorCards(WidgetRef ref, _AgentColors colors) { - const doctors = [ - { - 'name': '张明', - 'title': '主任医师', - 'dept': '心脏康复科', - 'desc': '冠心病、高血压术后管理', - 'id': '468b82e2-d95a-4436-bff6-a50eecf99a66', - }, - { - 'name': '李芳', - 'title': '副主任医师', - 'dept': '营养科', - 'desc': '糖尿病、甲状腺疾病管理', - 'id': 'd4148733-b538-4398-af17-0c7592fc0c2d', - }, - { - 'name': '王建国', - 'title': '主任医师', - 'dept': '心血管内科', - 'desc': '术后营养指导、饮食方案制定', - 'id': 'ef0953c9-eb63-4d03-b6d7-050a1897d4a3', - }, - ]; - return Row( - children: [ - for (var i = 0; i < doctors.length; i++) ...[ - if (i > 0) const SizedBox(width: 8), - Expanded(child: _doctorCard(doctors[i], ref, colors)), - ], - ], - ); - } - - Widget _doctorCard( - Map doc, - WidgetRef ref, - _AgentColors colors, - ) { - return InkWell( - onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}), - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: colors.border), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar( - radius: 22, - backgroundColor: colors.iconBg, - child: Text( - doc['name']![0], - style: TextStyle( - fontSize: 21, - fontWeight: FontWeight.w600, - color: colors.accent, - ), - ), - ), - const SizedBox(height: 6), - Text( - doc['name']!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - ), - Text( - doc['title']!, - style: const TextStyle(fontSize: 14, color: AppColors.textHint), - ), - const SizedBox(height: 2), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: colors.iconBg, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - doc['dept']!, - style: TextStyle(fontSize: 13, color: colors.accent), - ), - ), - const SizedBox(height: 4), - Text( - doc['desc']!, - style: const TextStyle( - fontSize: 13, - color: AppColors.textSecondary, - height: 1.3, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - // ═══════════════════════════════════════════════════════════ // 2. DataConfirmCard — 统一确认卡片(紫白蓝风格) // ═══════════════════════════════════════════════════════════ @@ -1556,46 +1470,52 @@ class ChatMessagesView extends ConsumerWidget { static _AgentColors _agentColors(ActiveAgent agent) { return switch (agent) { ActiveAgent.consultation => _AgentColors( - gradient: [AppColors.sageAccent, AppColors.primary], - bg: const Color(0xFFF2FBF9), - border: const Color(0xFFD9F0EA), - iconBg: const Color(0xFFE8F8F4), - accent: const Color(0xFF4F9E90), + gradient: [Color(0xFFFFCAD4), Color(0xFFFF9AAE)], + bg: const Color(0xFFFFF4F6), + border: const Color(0xFFFFDFE7), + iconBg: const Color(0xFFFFEEF2), + accent: const Color(0xFFFF7F98), + verticalGradient: true, ), ActiveAgent.health => _AgentColors( - gradient: [AppColors.meadowAccent, AppColors.auraDeepIndigo], - bg: const Color(0xFFF3FBF4), - border: const Color(0xFFDFF2E4), - iconBg: const Color(0xFFEFF9F0), - accent: const Color(0xFF4EA65B), + gradient: [Color(0xFFFFAFBD), Color(0xFFC9FFBF)], + bg: const Color(0xFFF7FDEB), + border: const Color(0xFFEAF7BF), + iconBg: const Color(0xFFF9FCEB), + accent: const Color(0xFF8BD982), + verticalGradient: true, ), ActiveAgent.diet => _AgentColors( - gradient: [AppColors.peachAccent, AppColors.auraOrchid], + gradient: [Color(0xFFF6D365), Color(0xFFFDA085)], bg: const Color(0xFFFFF7F0), border: const Color(0xFFFFE8D4), iconBg: const Color(0xFFFFF0E4), - accent: const Color(0xFFE26A64), + accent: const Color(0xFFF89C5B), + verticalGradient: true, ), ActiveAgent.medication => _AgentColors( - gradient: [AppColors.auraLavender, AppColors.blueMeasure], + gradient: [Color(0xFF89F7FE), Color(0xFF66A6FF)], bg: const Color(0xFFF4F6FF), border: const Color(0xFFDDE6FF), iconBg: const Color(0xFFEFF3FF), - accent: AppColors.primary, + accent: const Color(0xFF4F8FF7), + verticalGradient: true, ), ActiveAgent.report => _AgentColors( - gradient: [AppColors.primary, AppColors.roseAccent], + gradient: [Color(0xFFE0C3FC), Color(0xFF8EC5FC)], bg: const Color(0xFFF8F5FF), border: const Color(0xFFE9E0FF), iconBg: const Color(0xFFF2EDFF), - accent: AppColors.primary, + accent: const Color(0xFF9D8AF8), + verticalGradient: true, ), ActiveAgent.exercise => _AgentColors( - gradient: [AppColors.sageAccent, AppColors.auraDeepIndigo], - bg: const Color(0xFFF0FBFD), - border: const Color(0xFFD6EEF2), - iconBg: const Color(0xFFE8F7FA), - accent: const Color(0xFF2B8EA0), + gradient: [Color(0xFFB6F2FF), Color(0xFF7DD3FC)], + bg: const Color(0xFFF0FCFF), + border: const Color(0xFFD8F5FC), + iconBg: const Color(0xFFEAFBFF), + accent: const Color(0xFF38BDE8), + verticalGradient: true, ), _ => _AgentColors( gradient: [AppColors.primary, AppColors.blueMeasure], @@ -1609,16 +1529,16 @@ class ChatMessagesView extends ConsumerWidget { static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) { return switch (agent) { - ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'), - ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'), - ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'), + ActiveAgent.health => (LucideIcons.heartPulse, '记数据', '录入血压、血糖、心率等日常指标'), + ActiveAgent.diet => (LucideIcons.utensils, '拍饮食', '拍照识别食物热量和营养成分'), + ActiveAgent.medication => (LucideIcons.pill, '药管家', '管理药品、提醒服药、追踪用量'), ActiveAgent.consultation => ( - Icons.local_hospital, - '问诊', - '在线咨询医生,描述症状获取建议', + LucideIcons.messageCircle, + 'AI问诊', + 'AI智能问诊,描述症状获取建议', ), - ActiveAgent.report => (Icons.assignment, '报告分析', '上传体检报告,AI 辅助解读'), - ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'), + ActiveAgent.report => (LucideIcons.fileText, '报告分析', '上传体检报告,AI 辅助解读'), + ActiveAgent.exercise => (LucideIcons.activity, '运动', '制定运动计划,打卡记录进度'), _ => (Icons.forum_outlined, 'AI 助手', '您的智能健康管家'), }; } @@ -2031,13 +1951,17 @@ class _AgentMark extends StatelessWidget { decoration: BoxDecoration( gradient: LinearGradient( colors: colors.gradient, - begin: Alignment.topLeft, - end: Alignment.bottomRight, + begin: colors.verticalGradient + ? Alignment.topCenter + : Alignment.topLeft, + end: colors.verticalGradient + ? Alignment.bottomCenter + : Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(colors.verticalGradient ? 18 : 20), boxShadow: [ BoxShadow( - color: colors.accent.withValues(alpha: 0.24), + color: colors.accent.withValues(alpha: 0.18), blurRadius: 18, offset: const Offset(0, 8), ), @@ -2103,12 +2027,14 @@ class _AgentColors { final Color border; final Color iconBg; final Color accent; + final bool verticalGradient; const _AgentColors({ required this.gradient, required this.bg, required this.border, required this.iconBg, required this.accent, + this.verticalGradient = false, }); } diff --git a/health_app/lib/pages/profile/profile_page.dart b/health_app/lib/pages/profile/profile_page.dart index 143a822..4767e75 100644 --- a/health_app/lib/pages/profile/profile_page.dart +++ b/health_app/lib/pages/profile/profile_page.dart @@ -17,127 +17,85 @@ class ProfilePage extends ConsumerWidget { return GradientScaffold( appBar: AppBar( - backgroundColor: Colors.white.withValues(alpha: 0.9), - title: const Text('个人信息'), + backgroundColor: Colors.white.withValues(alpha: 0.86), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 19), + onPressed: () => popRoute(ref), + ), + title: const Text( + '个人信息', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800), + ), + centerTitle: true, ), body: SafeArea( child: SingleChildScrollView( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.fromLTRB(20, 18, 20, 34), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 20), - - // 头像区 - Center( - child: CircleAvatar( - radius: 50, - backgroundColor: AppColors.primaryLight, - backgroundImage: user?.avatarUrl != null - ? NetworkImage(user!.avatarUrl!) - : null, - child: user?.avatarUrl == null - ? const Icon( - Icons.person, - size: 48, - color: AppColors.textHint, - ) - : null, - ), + _ProfileHero( + name: name, + phone: phone, + avatarUrl: user?.avatarUrl, ), - const SizedBox(height: 20), - Text( - name, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - ), + const SizedBox(height: 18), + _InfoPanel( + children: [ + _InfoRow( + icon: Icons.badge_rounded, + label: '姓名', + value: name, + colors: const [Color(0xFF7DD3FC), Color(0xFF2563EB)], + ), + const _SoftDivider(), + _InfoRow( + icon: Icons.phone_iphone_rounded, + label: '手机号', + value: phone.isNotEmpty ? phone : '未绑定手机', + colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)], + ), + const _SoftDivider(), + _InfoRow( + icon: Icons.folder_shared_rounded, + label: '健康档案', + value: '查看和维护基础健康资料', + colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)], + onTap: () => pushRoute(ref, 'healthArchive'), + ), + ], ), - const SizedBox(height: 6), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 6, - ), - decoration: BoxDecoration( - gradient: AppColors.surfaceGradient, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.borderLight), - boxShadow: AppColors.cardShadowLight, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.phone_android, - size: 16, - color: AppColors.textSecondary, - ), - const SizedBox(width: 6), - Text( - phone.isNotEmpty ? phone : '未绑定手机', - style: const TextStyle( - fontSize: 14, - color: AppColors.textSecondary, - ), - ), - ], - ), + const SizedBox(height: 18), + _InfoPanel( + children: [ + _InfoRow( + icon: Icons.verified_user_rounded, + label: '隐私保护', + value: '健康数据仅用于个人健康管理', + colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)], + ), + ], ), - - const SizedBox(height: 32), - - // 基础信息卡片 - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: AppColors.surfaceGradient, - borderRadius: BorderRadius.circular(AppTheme.rLg), - border: Border.all(color: AppColors.borderLight), - boxShadow: AppColors.cardShadowLight, - ), - child: Column( - children: [ - _infoRow(Icons.person_outline, '姓名', name), - const Divider(height: 24, color: AppColors.borderLight), - _infoRow( - Icons.phone_android, - '手机号', - phone.isNotEmpty ? phone : '未绑定', - ), - const Divider(height: 24, color: AppColors.borderLight), - _infoRow( - Icons.health_and_safety, - '健康档案', - '查看详情', - onTap: () => pushRoute(ref, 'healthArchive'), - ), - ], - ), - ), - - const SizedBox(height: 40), - - // 退出登录 - SizedBox( - width: 200, - child: OutlinedButton.icon( - onPressed: () => _logout(context, ref), - icon: const Icon(Icons.logout, size: 18), - label: const Text('退出登录', style: TextStyle(fontSize: 16)), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.error, - side: const BorderSide(color: AppColors.error), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(vertical: 12), + const SizedBox(height: 28), + OutlinedButton.icon( + onPressed: () => _logout(context, ref), + icon: const Icon(Icons.logout_rounded, size: 19), + label: const Text('退出登录'), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.error, + side: BorderSide( + color: AppColors.error.withValues(alpha: 0.34), + ), + minimumSize: const Size.fromHeight(52), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), ), ), ), - - const SizedBox(height: 40), ], ), ), @@ -145,61 +103,6 @@ class ProfilePage extends ConsumerWidget { ); } - Widget _infoRow( - IconData icon, - String label, - String value, { - VoidCallback? onTap, - }) { - return GestureDetector( - onTap: onTap, - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(12), - boxShadow: AppColors.cardShadowLight, - ), - child: Icon(icon, size: 22, color: Colors.white), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: const TextStyle( - fontSize: 13, - color: AppColors.textHint, - ), - ), - const SizedBox(height: 3), - Text( - value, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: AppColors.textPrimary, - ), - ), - ], - ), - ), - if (onTap != null) - const Icon( - Icons.chevron_right, - size: 20, - color: AppColors.textHint, - ), - ], - ), - ); - } - Future _logout(BuildContext context, WidgetRef ref) async { final ok = await showDialog( context: context, @@ -227,3 +130,200 @@ class ProfilePage extends ConsumerWidget { } } } + +class _ProfileHero extends StatelessWidget { + final String name; + final String phone; + final String? avatarUrl; + const _ProfileHero({ + required this.name, + required this.phone, + required this.avatarUrl, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + 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), + ), + ], + ), + 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)], + ), + 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, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _InfoPanel extends StatelessWidget { + final List children; + const _InfoPanel({required this.children}); + + @override + Widget build(BuildContext context) { + 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), + boxShadow: AppColors.cardShadowLight, + ), + child: Column(children: children), + ); + } +} + +class _InfoRow extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final List colors; + final VoidCallback? onTap; + const _InfoRow({ + required this.icon, + required this.label, + required this.value, + required this.colors, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: colors, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Icon(icon, color: Colors.white, size: 22), + ), + const SizedBox(width: 13), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.textHint, + ), + ), + const SizedBox(height: 4), + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + ], + ), + ), + if (onTap != null) + const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: AppColors.textHint, + ), + ], + ), + ), + ); + } +} + +class _SoftDivider extends StatelessWidget { + const _SoftDivider(); + + @override + Widget build(BuildContext context) { + return const Divider(height: 1, color: Color(0xFFF1F3F8)); + } +} diff --git a/health_app/lib/pages/profile/service_package_detail_page.dart b/health_app/lib/pages/profile/service_package_detail_page.dart deleted file mode 100644 index e6dfd2a..0000000 --- a/health_app/lib/pages/profile/service_package_detail_page.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/app_colors.dart'; -import '../../core/app_theme.dart'; -import '../../widgets/service_package_card.dart'; - -class ServicePackageDetailPage extends ConsumerWidget { - final String packageId; - const ServicePackageDetailPage({super.key, required this.packageId}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final package = servicePackages.where((p) => p.id == packageId).firstOrNull; - if (package == null) { - return GradientScaffold( - appBar: AppBar(title: const Text('服务包详情')), - body: const Center(child: Text('未找到该服务包')), - ); - } - - return GradientScaffold( - appBar: AppBar( - title: Text(package.title, style: const TextStyle(fontSize: 19)), - centerTitle: true, - ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 头部卡片 - Container( - width: double.infinity, - margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - package.headerColor, - package.headerColor.withAlpha(180), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(20), - boxShadow: AppColors.cardShadow, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: Colors.white.withAlpha(40), - borderRadius: BorderRadius.circular(6), - ), - child: const Text( - 'VIP 产品权益', - style: TextStyle( - fontSize: 15, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(height: 12), - Text( - package.title, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - Text( - package.subtitle, - style: TextStyle( - fontSize: 17, - color: Colors.white.withAlpha(200), - ), - ), - ], - ), - ), - // 服务图标 - Padding( - padding: const EdgeInsets.all(20), - child: Wrap( - spacing: 16, - runSpacing: 16, - children: package.services - .map( - (s) => SizedBox( - width: (MediaQuery.of(context).size.width - 72) / 4, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: AppTheme.primaryLight, - borderRadius: BorderRadius.circular(14), - ), - child: Icon( - s.icon, - size: 25, - color: AppTheme.primary, - ), - ), - const SizedBox(height: 6), - Text( - s.label, - style: const TextStyle( - fontSize: 15, - color: AppTheme.textSub, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ) - .toList(), - ), - ), - // 适用人群 - _Section( - title: '适用人群', - child: Text( - package.targetAudience, - style: const TextStyle( - fontSize: 17, - color: AppTheme.textSub, - height: 1.6, - ), - ), - ), - // 详细说明 - ...package.detailSections.map( - (s) => _Section( - title: s.title, - child: Text( - s.content, - style: const TextStyle( - fontSize: 17, - color: AppTheme.textSub, - height: 1.6, - ), - ), - ), - ), - const SizedBox(height: 40), - ], - ), - ), - ); - } -} - -class _Section extends StatelessWidget { - final String title; - final Widget child; - const _Section({required this.title, required this.child}); - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.border), - boxShadow: AppColors.cardShadowLight, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 19, - fontWeight: FontWeight.w600, - color: AppTheme.text, - ), - ), - const SizedBox(height: 10), - child, - ], - ), - ); - } -} diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index 1489f56..34488ec 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -5,60 +5,364 @@ import '../../core/app_colors.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; -import '../../widgets/app_menu_item.dart'; +import '../../providers/data_providers.dart' show apiClientProvider; class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); - @override Widget build(BuildContext context, WidgetRef ref) { + @override + Widget build(BuildContext context, WidgetRef ref) { return GradientScaffold( appBar: AppBar( - backgroundColor: AppColors.cardBackground, - leading: IconButton(icon: const Icon(LucideIcons.chevronLeft, color: AppColors.textPrimary), onPressed: () => popRoute(ref)), - title: const Text('设置', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), + backgroundColor: Colors.white.withValues(alpha: 0.86), + leading: IconButton( + icon: const Icon( + LucideIcons.chevronLeft, + color: AppColors.textPrimary, + ), + onPressed: () => popRoute(ref), + ), + title: const Text( + '设置', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), centerTitle: true, ), - body: SafeArea(child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 30), - child: Column(children: [ - const SizedBox(height: AppTheme.sMd), - AppMenuItem(icon: LucideIcons.bell, title: '消息通知', onTap: () => pushRoute(ref, 'notificationPrefs')), - AppMenuItem(icon: LucideIcons.type, title: '字体大小', subtitle: '老年版', onTap: () {}), - AppMenuItem(icon: LucideIcons.sprayCan, title: '清除缓存', onTap: () {}), - AppMenuItem(icon: LucideIcons.info, title: '关于健康管家', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'})), - AppMenuItem(icon: LucideIcons.shield, title: '隐私协议', onTap: () => pushRoute(ref, 'staticText', params: {'type': 'privacy'})), - const SizedBox(height: 30), - GestureDetector( - onTap: () => _logout(context, ref), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: AppTheme.rXl), - height: 50, - alignment: Alignment.center, - decoration: BoxDecoration( - border: Border.all(color: AppColors.error.withAlpha(80)), - borderRadius: BorderRadius.circular(AppTheme.rPill), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 34), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const _SettingsHeader(), + const SizedBox(height: 18), + _SettingsGroup( + title: '健康与设备', + children: [ + _SettingsTile( + icon: LucideIcons.bluetooth, + title: '蓝牙设备', + colors: const [Color(0xFF38BDF8), Color(0xFF2563EB)], + onTap: () => pushRoute(ref, 'devices'), + ), + _SettingsTile( + icon: LucideIcons.bell, + title: '消息通知', + colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)], + onTap: () => pushRoute(ref, 'notificationPrefs'), + ), + ], ), - child: const Text('退出登录', style: TextStyle(fontSize: 19, color: AppColors.error, fontWeight: FontWeight.w500)), - ), + const SizedBox(height: 16), + _SettingsGroup( + title: '使用偏好', + children: [ + _SettingsTile( + icon: LucideIcons.type, + title: '字体大小', + subtitle: '老年友好', + colors: const [Color(0xFFFBBF24), Color(0xFFEA580C)], + onTap: () {}, + ), + _SettingsTile( + icon: LucideIcons.sprayCan, + title: '清除缓存', + colors: const [Color(0xFF2DD4BF), Color(0xFF0F766E)], + onTap: () {}, + ), + ], + ), + const SizedBox(height: 16), + _SettingsGroup( + title: '关于', + children: [ + _SettingsTile( + icon: LucideIcons.info, + title: '关于健康管家', + colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)], + onTap: () => + pushRoute(ref, 'staticText', params: {'type': 'about'}), + ), + _SettingsTile( + icon: LucideIcons.shield, + title: '隐私协议', + colors: const [Color(0xFFF472B6), Color(0xFFDB2777)], + onTap: () => pushRoute( + ref, + 'staticText', + params: {'type': 'privacy'}, + ), + ), + ], + ), + const SizedBox(height: 14), + OutlinedButton.icon( + onPressed: () => _deleteAccount(context, ref), + icon: const Icon(LucideIcons.trash, size: 19), + label: const Text('删除账号'), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.error, + side: BorderSide(color: AppColors.error.withValues(alpha: 0.34)), + minimumSize: const Size.fromHeight(52), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + ), + ), + const SizedBox(height: 10), + ElevatedButton.icon( + onPressed: () => _logout(context, ref), + icon: const Icon(LucideIcons.logOut, size: 19), + label: const Text('退出登录'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.error, + foregroundColor: Colors.white, + minimumSize: const Size.fromHeight(52), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + ), + ], ), - ]), - )), + ), + ), ); } + Future _deleteAccount(BuildContext context, WidgetRef ref) async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)), + title: const Text('删除账号', style: TextStyle(color: AppColors.error)), + content: const Text('此操作不可恢复,将删除所有健康数据、用药记录、饮食记录、报告等,确定继续?'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), + TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确认删除', style: TextStyle(color: AppColors.error))), + ], + ), + ); + if (ok == true) { + await ref.read(apiClientProvider).delete('/api/user/account'); + await ref.read(authProvider.notifier).logout(); + goRoute(ref, 'login'); + } + } + Future _logout(BuildContext context, WidgetRef ref) async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.rXl)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppTheme.rXl), + ), title: const Text('退出登录'), - content: const Text('确定退出?'), + content: const Text('确定退出当前账号?'), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')), - TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('确定', style: TextStyle(color: AppColors.error))), + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('确定', style: TextStyle(color: AppColors.error)), + ), ], ), ); - if (ok == true) { await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } + if (ok == true) { + await ref.read(authProvider.notifier).logout(); + goRoute(ref, 'login'); + } + } +} + +class _SettingsHeader extends StatelessWidget { + const _SettingsHeader(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFEEF2FF), Color(0xFFF7ECFF), Color(0xFFEFFBF9)], + ), + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Colors.white, width: 1.5), + ), + child: Row( + children: [ + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF38BDF8), Color(0xFFC084FC)], + ), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + LucideIcons.settings, + color: Colors.white, + size: 25, + ), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '偏好设置', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + SizedBox(height: 5), + Text( + '管理设备、通知和账号安全', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SettingsGroup extends StatelessWidget { + final String title; + final List children; + const _SettingsGroup({required this.title, required this.children}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900, + color: AppColors.textSecondary, + ), + ), + ), + 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), + boxShadow: AppColors.cardShadowLight, + ), + child: Column( + children: [ + for (var i = 0; i < children.length; i++) ...[ + children[i], + if (i != children.length - 1) + const Divider(height: 1, color: Color(0xFFF1F3F8)), + ], + ], + ), + ), + ], + ); + } +} + +class _SettingsTile extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final List colors; + final VoidCallback onTap; + const _SettingsTile({ + required this.icon, + required this.title, + required this.colors, + required this.onTap, + this.subtitle, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 12), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: colors, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Icon(icon, color: Colors.white, size: 21), + ), + const SizedBox(width: 13), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 3), + Text( + subtitle!, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.textHint, + ), + ), + ], + ], + ), + ), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: AppColors.textHint, + ), + ], + ), + ), + ); } } diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index f044448..719ae43 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -4,7 +4,6 @@ import '../core/app_colors.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/data_providers.dart'; -import 'service_package_card.dart'; class HealthDrawer extends ConsumerWidget { const HealthDrawer({super.key}); @@ -12,32 +11,36 @@ class HealthDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final auth = ref.watch(authProvider); - final user = auth.user; final latestHealth = ref.watch(latestHealthProvider); return Drawer( - width: MediaQuery.of(context).size.width * 0.88, + width: MediaQuery.of(context).size.width * 0.9, backgroundColor: Colors.transparent, elevation: 0, - child: Container( - decoration: const BoxDecoration(gradient: AppColors.drawerGradient), + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFE0C3FC), Color(0xFFFFFFFF), Color(0xFFDCEEFF)], + stops: [0.0, 0.48, 1.0], + ), + ), child: SafeArea( child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 22), + padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), sliver: SliverList.list( children: [ - _AccountHero(user: user, ref: ref), - const SizedBox(height: 14), - _PrimaryActions(ref: ref), - const SizedBox(height: 14), + _AccountHeader(user: auth.user, ref: ref), + const SizedBox(height: 18), + _ArchiveAction(ref: ref), + const SizedBox(height: 18), _HealthDashboard(latestHealth: latestHealth, ref: ref), - const SizedBox(height: 14), + const SizedBox(height: 18), _NavigationSection(ref: ref), - const SizedBox(height: 14), - const _ServiceSection(), ], ), ), @@ -49,10 +52,10 @@ class HealthDrawer extends ConsumerWidget { } } -class _AccountHero extends StatelessWidget { +class _AccountHeader extends StatelessWidget { final dynamic user; final WidgetRef ref; - const _AccountHero({required this.user, required this.ref}); + const _AccountHeader({required this.user, required this.ref}); @override Widget build(BuildContext context) { @@ -63,230 +66,133 @@ class _AccountHero extends StatelessWidget { ? user.phone.toString() : '未登录'; - return Container( - padding: const EdgeInsets.fromLTRB(16, 16, 12, 16), - decoration: BoxDecoration( - color: Colors.white, - 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), - ), - ], - ), - 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, - ), - ), - ), - ), - 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), + return Row( + children: [ + InkWell( + onTap: () => pushRoute(ref, 'profile'), + borderRadius: BorderRadius.circular(24), + child: Container( + width: 66, + height: 66, decoration: BoxDecoration( gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [Color(0xFFF7F4FF), Color(0xFFF2FBF9)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFA5F3FC), Color(0xFFD8B4FE)], ), - 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, - ), - ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: const Color(0xFF7C3AED).withValues(alpha: 0.12), + blurRadius: 22, + offset: const Offset(0, 12), ), ], ), - ), - ], - ), - ); - } -} - -class _PrimaryActions extends StatelessWidget { - final WidgetRef ref; - const _PrimaryActions({required this.ref}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: _FeaturedAction( - icon: Icons.bluetooth_connected_rounded, - title: '蓝牙设备', - subtitle: '连接血压计', - gradient: AppColors.calmHealthGradient, - onTap: () => pushRoute(ref, 'devices'), + child: const Icon( + Icons.person_rounded, + color: Colors.white, + size: 34, + ), ), ), - const SizedBox(width: 10), + const SizedBox(width: 14), Expanded( - child: _FeaturedAction( - icon: Icons.folder_shared_outlined, - title: '健康档案', - subtitle: '资料与病史', - gradient: AppColors.primaryGradient, - onTap: () => pushRoute(ref, 'healthArchive'), + child: InkWell( + onTap: () => pushRoute(ref, 'profile'), + borderRadius: BorderRadius.circular(14), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 5), + Text( + phone, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), ), ), + _IconButton( + icon: Icons.settings_rounded, + onTap: () => pushRoute(ref, 'settings'), + ), ], ); } } -class _FeaturedAction extends StatelessWidget { - final IconData icon; - final String title; - final String subtitle; - final LinearGradient gradient; - final VoidCallback onTap; - const _FeaturedAction({ - required this.icon, - required this.title, - required this.subtitle, - required this.gradient, - required this.onTap, - }); +class _ArchiveAction extends StatelessWidget { + final WidgetRef ref; + const _ArchiveAction({required this.ref}); @override Widget build(BuildContext context) { - 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, + return InkWell( + onTap: () => pushRoute(ref, 'healthArchive'), + borderRadius: BorderRadius.circular(24), + child: Container( + padding: const EdgeInsets.fromLTRB(16, 14, 14, 14), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Color(0xFFF3ECFF), Color(0xFFEDE4FF)], ), - child: Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.96), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white, width: 1.4), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.82), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.folder_shared_rounded, + color: Color(0xFF14B8A6), + size: 23, + ), ), - 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(width: 12), + const Expanded( + child: Text( + '健康档案', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, ), - 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, - ), - ), - ], + ), ), - ), + const Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: AppColors.textHint, + ), + ], ), ), ); @@ -300,159 +206,83 @@ class _HealthDashboard extends StatelessWidget { @override Widget build(BuildContext context) { - return _SectionShell( + return _Panel( title: '健康仪表盘', - subtitle: '最近一次健康数据', - icon: Icons.query_stats_rounded, - trailing: _TextPillButton( + trailing: _TextAction( label: '详情', + light: true, onTap: () => pushRoute(ref, 'trend'), ), + titleColor: Colors.white, + backgroundGradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF00F2FE), Color(0xFF4FACFE)], + ), child: latestHealth.when( - data: (data) => _DashboardBody(data: data, ref: ref), - loading: () => const Padding( - padding: EdgeInsets.all(24), + data: (data) => _DashboardMetrics(data: data, ref: ref), + loading: () => const SizedBox( + height: 118, child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), - error: (_, __) => - _DashboardBody(data: const {}, ref: ref), + error: (error, stackTrace) => + _DashboardMetrics(data: const {}, ref: ref), ), ); } } -class _DashboardBody extends StatelessWidget { +class _DashboardMetrics extends StatelessWidget { final Map data; final WidgetRef ref; - const _DashboardBody({required this.data, required this.ref}); + const _DashboardMetrics({required this.data, required this.ref}); @override Widget build(BuildContext context) { - final bp = _bpText(data['BloodPressure']); - final metrics = [ + final items = [ + _MetricInfo( + label: '血压', + value: _bpText(data['BloodPressure']), + routeType: 'blood_pressure', + ), _MetricInfo( - icon: Icons.monitor_heart_outlined, label: '心率', - value: _metricVal(data['HeartRate'], unit: 'bpm'), + value: _metricVal(data['HeartRate']), + unit: 'bpm', routeType: 'heart_rate', ), _MetricInfo( - icon: Icons.water_drop_outlined, label: '血糖', - value: _metricVal(data['Glucose'], unit: 'mmol/L'), + value: _metricVal(data['Glucose']), + unit: 'mmol/L', routeType: 'glucose', ), _MetricInfo( - icon: Icons.air_outlined, label: '血氧', - value: _metricVal(data['SpO2'], unit: '%'), + value: _metricVal(data['SpO2']), + unit: '%', routeType: 'spo2', ), _MetricInfo( - icon: Icons.monitor_weight_outlined, label: '体重', - value: _metricVal(data['Weight'], unit: 'kg'), + value: _metricVal(data['Weight']), + unit: 'kg', routeType: 'weight', ), ]; - return Column( + return Row( 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, - ), - ], - ), - ), + for (var i = 0; i < items.length; i++) ...[ + Expanded( + child: _MetricTile( + info: items[i], + onTap: () => + pushRoute(ref, 'trend', params: {'type': items[i].routeType}), ), ), - ), - 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(), - ); - }, - ), + if (i != items.length - 1) const SizedBox(width: 6), + ], ], ); } @@ -482,75 +312,56 @@ class _DashboardBody extends StatelessWidget { class _MetricTile extends StatelessWidget { final _MetricInfo info; - final double width; final VoidCallback onTap; - const _MetricTile({ - required this.info, - required this.width, - required this.onTap, - }); + const _MetricTile({required this.info, required this.onTap}); @override Widget build(BuildContext context) { - return SizedBox( - width: width, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Container( + height: 92, + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.30), 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), - ), - 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, + border: Border.all(color: Colors.white.withValues(alpha: 0.50)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 28, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + info.value, + maxLines: 1, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.white, ), ), - 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, - ), - ), - ], - ), - ), - ], + ), ), - ), + if (info.unit != null) + Text( + info.unit!, + style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF)), + ), + const SizedBox(height: 2), + Text( + info.label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: Colors.white, + ), + ), + ], ), ), ); @@ -565,158 +376,146 @@ class _NavigationSection extends StatelessWidget { Widget build(BuildContext context) { final items = [ _NavItem( - icon: Icons.description_outlined, - title: '报告管理', - subtitle: '上传、查看和分析报告', + icon: Icons.description_rounded, + title: '报告', route: 'reports', + colors: const [Color(0xFFBFDBFE), Color(0xFF93C5FD)], ), _NavItem( - icon: Icons.medication_liquid_outlined, - title: '用药管理', - subtitle: '药品记录与服药打卡', + icon: Icons.medication_rounded, + title: '用药', route: 'medications', + colors: const [Color(0xFFDDD6FE), Color(0xFFC4B5FD)], ), _NavItem( - icon: Icons.restaurant_menu_outlined, - title: '饮食记录', - subtitle: '查看识别结果和历史饮食', + icon: Icons.restaurant_rounded, + title: '饮食', route: 'dietRecords', + colors: const [Color(0xFFFED7AA), Color(0xFFFDBA74)], ), _NavItem( - icon: Icons.calendar_month_outlined, - title: '健康日历', - subtitle: '按日期查看健康记录', + icon: Icons.calendar_month_rounded, + title: '日历', route: 'calendar', + colors: const [Color(0xFFA7F3D0), Color(0xFF86EFAC)], ), _NavItem( - icon: Icons.event_note_outlined, - title: '复查随访', - subtitle: '随访计划和复查提醒', + icon: Icons.event_available_rounded, + title: '随访', route: 'followups', + colors: const [Color(0xFFFBCFE8), Color(0xFFF9A8D4)], ), _NavItem( - icon: Icons.directions_run_outlined, - title: '运动计划', - subtitle: '计划、打卡和进度', + icon: Icons.directions_run_rounded, + title: '运动', route: 'exercisePlan', + colors: const [Color(0xFFA5F3FC), Color(0xFF67E8F9)], ), ]; - return _SectionShell( + return _Panel( 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), - ), - ], - ], + child: GridView.builder( + itemCount: items.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisExtent: 98, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemBuilder: (context, index) { + final item = items[index]; + return _NavTile(item: item, onTap: () => pushRoute(ref, item.route)); + }, ), ); } } -class _NavigationRow extends StatelessWidget { +class _NavTile extends StatelessWidget { final _NavItem item; final VoidCallback onTap; - const _NavigationRow({required this.item, required this.onTap}); + const _NavTile({required this.item, required this.onTap}); + + String get _title => switch (item.route) { + 'reports' => '报告管理', + 'medications' => '用药管理', + 'dietRecords' => '饮食记录', + 'calendar' => '健康日历', + 'followups' => '复查随访', + 'exercisePlan' => '运动计划', + _ => item.title, + }; + + List get _colors => switch (item.route) { + 'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)], + 'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)], + 'dietRecords' => const [Color(0xFFF6D365), Color(0xFFF97316)], + 'calendar' => const [Color(0xFF34D399), Color(0xFF059669)], + 'followups' => const [Color(0xFFF472B6), Color(0xFFDB2777)], + 'exercisePlan' => const [Color(0xFF22D3EE), Color(0xFF0891B2)], + _ => item.colors, + }; @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), + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.78), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white, width: 1.2), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _colors, ), - child: Icon(item.icon, size: 20, color: Colors.white), + borderRadius: BorderRadius.circular(16), ), - 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, - ), - ), - ], - ), + child: Icon(item.icon, color: Colors.white, size: 24), + ), + const SizedBox(height: 9), + Text( + _title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, ), - 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 { +class _Panel extends StatelessWidget { final String title; - final String subtitle; - final IconData icon; final Widget child; final Widget? trailing; - const _SectionShell({ + final LinearGradient? backgroundGradient; + final Color titleColor; + const _Panel({ required this.title, - required this.subtitle, - required this.icon, required this.child, this.trailing, + this.backgroundGradient, + this.titleColor = AppColors.textPrimary, }); @override @@ -724,54 +523,45 @@ class _SectionShell extends StatelessWidget { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.96), - borderRadius: BorderRadius.circular(26), - border: Border.all(color: Colors.white, width: 1.5), - boxShadow: AppColors.cardShadowLight, + gradient: + backgroundGradient ?? + LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withValues(alpha: 0.92), + const Color(0xFFF8F4FF).withValues(alpha: 0.84), + ], + ), + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Colors.white, width: 1.4), + boxShadow: [ + BoxShadow( + color: const Color(0xFF6D5DF6).withValues(alpha: 0.08), + blurRadius: 22, + offset: const Offset(0, 12), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Container( - width: 38, - height: 38, - decoration: BoxDecoration( - gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(14), - ), - 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, - ), - ), - ], + child: Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: titleColor, + ), ), ), - if (trailing != null) trailing!, + ?trailing, ], ), - const SizedBox(height: 14), + const SizedBox(height: 12), child, ], ), @@ -779,32 +569,38 @@ class _SectionShell extends StatelessWidget { } } -class _TextPillButton extends StatelessWidget { +class _TextAction extends StatelessWidget { final String label; final VoidCallback onTap; - const _TextPillButton({required this.label, required this.onTap}); + final bool light; + const _TextAction({ + required this.label, + required this.onTap, + this.light = false, + }); @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, - ), + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(999), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 6), + decoration: BoxDecoration( + color: light + ? Colors.white.withValues(alpha: 0.24) + : const Color(0xFFEDE9FE), + borderRadius: BorderRadius.circular(999), + border: light + ? Border.all(color: Colors.white.withValues(alpha: 0.34)) + : null, + ), + child: Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w900, + color: light ? Colors.white : Color(0xFF6D28D9), ), ), ), @@ -812,42 +608,39 @@ class _TextPillButton extends StatelessWidget { } } -class _CircleIconButton extends StatelessWidget { +class _IconButton extends StatelessWidget { final IconData icon; final VoidCallback onTap; - const _CircleIconButton({required this.icon, required this.onTap}); + const _IconButton({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), + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.84), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Colors.white, width: 1.2), ), + child: Icon(icon, size: 22, color: const Color(0xFF475569)), ), ); } } class _MetricInfo { - final IconData icon; final String label; final String value; + final String? unit; final String routeType; const _MetricInfo({ - required this.icon, required this.label, required this.value, + this.unit, required this.routeType, }); } @@ -855,12 +648,12 @@ class _MetricInfo { class _NavItem { final IconData icon; final String title; - final String subtitle; final String route; + final List colors; const _NavItem({ required this.icon, required this.title, - required this.subtitle, required this.route, + required this.colors, }); } diff --git a/health_app/lib/widgets/service_package_card.dart b/health_app/lib/widgets/service_package_card.dart deleted file mode 100644 index 9fc08bc..0000000 --- a/health_app/lib/widgets/service_package_card.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../core/app_theme.dart'; -import '../../core/navigation_provider.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -/// 服务包数据模型 -class ServicePackage { - final String id; - final String title; - final String subtitle; - final Color headerColor; - final List services; - final String targetAudience; - final List detailSections; - - const ServicePackage({ - required this.id, - required this.title, - required this.subtitle, - required this.headerColor, - required this.services, - required this.targetAudience, - required this.detailSections, - }); -} - -class ServiceItem { - final IconData icon; - final String label; - - const ServiceItem({required this.icon, required this.label}); -} - -class DetailSection { - final String title; - final String content; - - const DetailSection({required this.title, required this.content}); -} - -/// 预定义服务包数据 —— 基于项目实际功能 -final List servicePackages = [ - ServicePackage( - id: 'vip_comprehensive', - title: '心血管健康管理服务包', - subtitle: 'VIP 产品权益', - headerColor: const Color(0xFF4A90D9), - services: [ - ServiceItem(icon: Icons.phone_in_talk_outlined, label: '电话咨询'), - ServiceItem(icon: Icons.chat_bubble_outline, label: '在线咨询'), - ServiceItem(icon: Icons.calendar_month_outlined, label: '个性化随访'), - ServiceItem(icon: Icons.medication_outlined, label: '调药管理'), - ServiceItem(icon: Icons.monitor_heart_outlined, label: '风险监测'), - ServiceItem(icon: Icons.devices_outlined, label: '智能硬件'), - ServiceItem(icon: Icons.family_restroom_outlined, label: '亲情账号'), - ServiceItem(icon: Icons.verified_user_outlined, label: '健康保障'), - ], - targetAudience: - '心血管疾病患者(如高血压、冠心病、心律失常等)\n' - '高危人群(如高血脂、糖尿病、肥胖、长期吸烟饮酒者)\n' - '术后康复人群(如心脏支架/搭桥术后、PCI术后患者)', - detailSections: [ - DetailSection( - title: '1、个性化康复管理', - content: - '设定动态化的危险因素管理目标,根据患者的住院数据(如基础疾病、高危因素、合并症、当前用药等),确定高危因素(如高脂血症、高血糖等),设定高危因子(如低密度脂蛋白等)的管控目标。基于药物不良反应,制定个性化管理及复查方案。', - ), - DetailSection( - title: '2、家庭医生专业团队', - content: - '由心内科主诊医生团队联合康复管理团队共同参与患者管理,将疾病康复从院内延伸至院外。团队由专科医生、康复治疗师、健康管理师、营养师等多学科成员组成。', - ), - DetailSection( - title: '3、全年不限次数在线咨询', - content: - '为患者提供全年不限次数的咨询服务,支持微信(如文字、图文、语音等)、电话咨询等方式。如检查报告解读、用药咨询、病情咨询、饮食咨询、心理咨询等。', - ), - DetailSection( - title: '4、主动跟踪随访管理', - content: - '动态临床评估:动态评估疾病复发、出血、致死致残等风险,实时优化管理方案。\n' - '主动症状管理:全年主动跟踪患者临床症状,实现疾病恶化早发现、早处理。\n' - '精准药物管理:全程用药指导,严密监控药物副作用,及时处理药物不良反应,确保药物治疗效果。\n' - '生活方式干预:个性化指导患者合理膳食、运动康复、心理调节、戒烟限酒等。', - ), - DetailSection( - title: '5、可穿戴智能设备', - content: - '配备可穿戴智能监测设备,患者居家测量后实时上传管理中心,医生和家属均可远程实时监测数据(血压和心率),记录数据变化趋势,智能预警,及时干预异常指标,降低不良事件发生率。', - ), - DetailSection( - title: '6、亲情账号联动管理', - content: - '支持5名家属参与管理,实时多端同步患者病情,医生、患者、家属三方共享,患者安心,家属放心。', - ), - DetailSection( - title: '7、健康档案与报告分析', - content: - '自动整合血压、心率、血糖、血氧、体重等健康数据,生成可视化健康趋势报告。\n' - 'AI 智能分析健康数据变化,提前预警潜在风险,提供个性化健康建议。', - ), - ], - ), - ServicePackage( - id: 'vip_premium', - title: '心力衰竭专项管理服务包', - subtitle: 'VIP 产品权益', - headerColor: const Color(0xFF2BA87E), - services: [ - ServiceItem(icon: Icons.phone_in_talk_outlined, label: '电话咨询'), - ServiceItem(icon: Icons.chat_bubble_outline, label: '在线咨询'), - ServiceItem(icon: Icons.calendar_month_outlined, label: '个性化随访'), - ServiceItem(icon: Icons.medication_outlined, label: '调药管理'), - ServiceItem(icon: Icons.monitor_heart_outlined, label: '风险监测'), - ServiceItem(icon: Icons.devices_outlined, label: '智能硬件'), - ServiceItem(icon: Icons.family_restroom_outlined, label: '亲情账号'), - ], - targetAudience: - '心力衰竭患者\n' - '心力衰竭高危人群(心肌梗死、瓣膜病、心肌病、高血压、代谢综合征等)', - detailSections: [ - DetailSection( - title: '1、个性化康复管理', - content: - '设定动态化的危险因素管理目标,根据患者的住院数据(如基础疾病、高危因素、合并症、当前用药等),确定高危因素管控目标。基于药物不良反应,制定个性化管理及复查方案。', - ), - DetailSection( - title: '2、家庭医生专业团队', - content: - '由心内科主诊医生团队联合哈瑞特医疗院外康复管理团队共同参与患者管理,将疾病康复从院内延伸至院外,哈瑞特医疗康复管理团队由专科医生、康复治疗师、健康管理师、营养师等多学科成员组成。', - ), - DetailSection( - title: '3、全年不限次数在线咨询', - content: - '为患者提供全年不限次数的咨询服务,支持微信(如文字、图文、语音等)、电话咨询(400-1666199),如检查报告解读、用药咨询、病情咨询、饮食咨询、心理咨询等。', - ), - DetailSection( - title: '4、主动跟踪随访管理', - content: - '动态临床评估:动态评估疾病复发、出血、致死致残等风险,实时优化管理方案。\n' - '主动症状管理:全年主动跟踪患者临床症状,实现疾病恶化早发现、早处理。\n' - '精准药物管理:全程用药指导,严密监控药物副作用,及时处理药物不良反应,确保药物治疗效果。\n' - '生活方式干预:个性化指导患者合理膳食、运动康复、心理调节、戒烟限酒等。', - ), - DetailSection( - title: '5、可穿戴智能设备', - content: - '配备可穿戴智能监测设备,患者居家测量后实时上传管理中心,医生和家属均可远程实时监测数据(血压和心率),记录数据变化趋势,智能预警,及时干预异常指标,降低不良事件发生率。', - ), - DetailSection( - title: '6、亲情账号联动管理', - content: - '支持5名家属参与管理,实时多端同步患者病情,医生、患者、家属三方共享,患者安心,家属放心。', - ), - ], - ), -]; - -/// 服务包卡片 -class ServicePackageCard extends ConsumerWidget { - final bool compact; - const ServicePackageCard({super.key, this.compact = false}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final package = servicePackages.first; - - // 构建服务图标网格 - final services = package.services.take(8).toList(); - final serviceRows = []; - for (var i = 0; i < services.length; i += 4) { - final rowItems = services.skip(i).take(4).toList(); - serviceRows.add(Padding( - padding: EdgeInsets.only(top: i > 0 ? 12 : 0), - child: Row( - children: rowItems.map((item) { - return Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: Color(0xFFFFF8EE), - shape: BoxShape.circle, - ), - child: Icon(item.icon, size: 23, color: const Color(0xFFF5A623)), - ), - const SizedBox(height: 4), - Text(item.label, - style: const TextStyle(fontSize: 14, color: AppTheme.textSub), - textAlign: TextAlign.center, - ), - ], - ), - ); - }).toList(), - ), - )); - } - - Widget card = GestureDetector( - onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}), - child: Padding( - padding: const EdgeInsets.fromLTRB(18, 16, 18, 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题行 + 详情入口 - Row(children: [ - Text(package.title, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: AppTheme.text)), - const Spacer(), - GestureDetector( - onTap: () => pushRoute(ref, 'servicePackageDetail', params: {'id': package.id}), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Text('详情', style: TextStyle(fontSize: 16, color: AppTheme.textSub)), - const SizedBox(width: 2), - Icon(Icons.chevron_right, size: 21, color: AppTheme.textHint), - ]), - ), - ]), - const SizedBox(height: 12), - // 服务图标网格 - ...serviceRows, - ], - ), - ), - ); - - if (compact) return card; - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(8), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: card, - ); - } -}