diff --git a/health_app/lib/app.dart b/health_app/lib/app.dart index 2fa4d11..a9837ee 100644 --- a/health_app/lib/app.dart +++ b/health_app/lib/app.dart @@ -8,9 +8,11 @@ import 'core/app_theme.dart'; import 'core/elder_mode_scope.dart'; import 'core/navigation_provider.dart'; import 'pages/splash_page.dart'; +import 'providers/ai_consent_provider.dart'; import 'providers/auth_provider.dart'; import 'providers/data_providers.dart'; import 'providers/elder_mode_provider.dart'; +import 'widgets/ai_consent_gate.dart'; /// 健康管家 App 根组件 class HealthApp extends ConsumerWidget { @@ -51,7 +53,7 @@ class HealthApp extends ConsumerWidget { child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: _BootGate(child: child!), + child: AiConsentGate(child: _BootGate(child: child!)), ), ), ), @@ -71,6 +73,15 @@ final appReadyProvider = Provider((ref) { if (currentRoute == 'login') return false; // 等根导航切到已登录首页后再撤 Splash final role = auth.user?.role ?? 'User'; if (role != 'User') return true; // 医生/管理员首页不依赖今日健康卡 + final consent = ref.watch(aiConsentProvider); + if (consent.userId == auth.user?.id && + !consent.isLoading && + !consent.granted) { + return true; + } + if (currentRoute == 'aiConsentDetails' || currentRoute == 'staticText') { + return true; + } if (!ref.watch(elderModeProvider).isLoaded) return false; // 普通用户:等今日健康卡片数据(成功或失败都算就绪,避免卡死) final health = ref.watch(latestHealthProvider); diff --git a/health_app/lib/core/api_client.dart b/health_app/lib/core/api_client.dart index 030278d..2b8d315 100644 --- a/health_app/lib/core/api_client.dart +++ b/health_app/lib/core/api_client.dart @@ -9,9 +9,22 @@ const String baseUrl = String.fromEnvironment( 'API_BASE_URL', defaultValue: kReleaseMode ? 'https://erpapi.datalumina.cn/xiaomai' - : 'http://192.168.1.34:5000', + : 'http://10.4.232.91:5000', ); +const String _configuredAppPolicy = String.fromEnvironment( + 'APP_POLICY_PROFILE', + defaultValue: '', +); + +String get appPolicyProfile { + if (_configuredAppPolicy == 'android-standard' || + _configuredAppPolicy == 'ios-medical-citations') { + return _configuredAppPolicy; + } + return Platform.isIOS ? 'ios-medical-citations' : 'android-standard'; +} + class ApiException implements Exception { final int? code; final String message; @@ -56,7 +69,10 @@ class ApiClient { baseUrl: baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 60), - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + 'X-App-Policy': appPolicyProfile, + }, ), ) { _dio.interceptors.add(_AuthInterceptor(this)); diff --git a/health_app/lib/pages/home/home_page.dart b/health_app/lib/pages/home/home_page.dart index 7a9ec18..db33417 100644 --- a/health_app/lib/pages/home/home_page.dart +++ b/health_app/lib/pages/home/home_page.dart @@ -438,9 +438,9 @@ class _HomePageState extends ConsumerState child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), + const SizedBox(height: 4), _buildAgentBar(), - const SizedBox(height: 12), + const SizedBox(height: 5), if (_pickedImagePath != null) _buildImagePreview(), _buildInputBar(), ], @@ -490,13 +490,13 @@ class _HomePageState extends ConsumerState chatProvider.select((state) => state.isStreaming), ); return Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), child: Container( padding: EdgeInsets.fromLTRB( 6, - elderMode ? 7 : 5, + elderMode ? 5 : 3, 6, - elderMode ? 7 : 5, + elderMode ? 5 : 3, ), decoration: BoxDecoration( color: Colors.white, @@ -540,7 +540,7 @@ class _HomePageState extends ConsumerState isDense: true, contentPadding: EdgeInsets.symmetric( horizontal: 4, - vertical: elderMode ? 15 : 12, + vertical: elderMode ? 12 : 9, ), border: InputBorder.none, enabledBorder: InputBorder.none, @@ -971,7 +971,7 @@ class _VoiceHoldSurface extends StatelessWidget { return Container( padding: EdgeInsets.symmetric( horizontal: 4, - vertical: elderMode ? 14 : 11, + vertical: elderMode ? 12 : 9, ), alignment: Alignment.center, child: Text( 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 bb12c9e..c501d8c 100644 --- a/health_app/lib/pages/home/widgets/chat_messages_view.dart +++ b/health_app/lib/pages/home/widgets/chat_messages_view.dart @@ -1276,7 +1276,6 @@ class ChatMessagesView extends ConsumerWidget { /// 处理 AI 回复里的 markdown 链接点击: /// - app://diet → 触发拍照/相册选择,跳到饮食拍照流程 /// - app://report → 跳到报告列表(用户可在那里上传新报告) - /// - app://device → 跳到蓝牙设备页 /// - 其他 app://xxx → 当作 route name 直接跳 static void _handleMarkdownLink( BuildContext context, @@ -1296,8 +1295,7 @@ class ChatMessagesView extends ConsumerWidget { pushRoute(ref, 'reports'); break; case 'device': - pushRoute(ref, 'devices'); - break; + return; default: if (uri.host.isNotEmpty) pushRoute(ref, uri.host); } @@ -2199,12 +2197,6 @@ final _agentActions = >{ isWide: true, route: 'trend', ), - _AgentAction( - label: '蓝牙录入', - icon: Icons.bluetooth, - isWide: true, - route: 'devices', - ), ], ActiveAgent.diet: [ _AgentAction( diff --git a/health_app/lib/pages/settings/ai_consent_details_page.dart b/health_app/lib/pages/settings/ai_consent_details_page.dart index 1310f0b..89a18e8 100644 --- a/health_app/lib/pages/settings/ai_consent_details_page.dart +++ b/health_app/lib/pages/settings/ai_consent_details_page.dart @@ -32,7 +32,15 @@ class AiConsentDetailsPage extends ConsumerWidget { ); if (confirmed != true || !context.mounted) return; - await ref.read(aiConsentProvider.notifier).revoke(userId); + final revoked = await ref.read(aiConsentProvider.notifier).revoke(userId); + if (!revoked) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Authorization could not be revoked.')), + ); + } + return; + } await ref.read(authProvider.notifier).logout(); } @@ -59,6 +67,7 @@ class AiConsentDetailsPage extends ConsumerWidget { color: AppColors.textPrimary, ), ), + centerTitle: true, ), body: SafeArea( child: SingleChildScrollView( @@ -91,8 +100,11 @@ class AiConsentDetailsPage extends ConsumerWidget { SizedBox( width: double.infinity, child: OutlinedButton( - onPressed: () => _revoke(context, ref), + onPressed: consent.isSaving + ? null + : () => _revoke(context, ref), style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, foregroundColor: AppColors.error, side: BorderSide( color: AppColors.error.withValues(alpha: 0.4), diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index 65e8b7b..9ae321d 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -56,13 +55,11 @@ class SettingsPage extends ConsumerWidget { title: '长辈模式', onTap: () => pushRoute(ref, 'elderMode'), ), - if (!Platform.isIOS) - // iOS 审核版:蓝牙设备入口已移除 - _SettingsTile( - icon: LucideIcons.bell, - title: '消息通知', - onTap: () => pushRoute(ref, 'notificationPrefs'), - ), + _SettingsTile( + icon: LucideIcons.bell, + title: '消息通知', + onTap: () => pushRoute(ref, 'notificationPrefs'), + ), _SettingsTile( icon: LucideIcons.info, title: '关于小脉健康', diff --git a/health_app/lib/providers/ai_consent_provider.dart b/health_app/lib/providers/ai_consent_provider.dart index 75510d8..8395846 100644 --- a/health_app/lib/providers/ai_consent_provider.dart +++ b/health_app/lib/providers/ai_consent_provider.dart @@ -6,22 +6,18 @@ const aiConsentVersion = 'ai-data-consent-v1'; class AiConsentState { final bool isLoading; + final bool isSaving; final bool granted; final String? userId; + final String? error; const AiConsentState({ this.isLoading = true, + this.isSaving = false, this.granted = false, this.userId, + this.error, }); - - AiConsentState copyWith({bool? isLoading, bool? granted, String? userId}) { - return AiConsentState( - isLoading: isLoading ?? this.isLoading, - granted: granted ?? this.granted, - userId: userId ?? this.userId, - ); - } } final aiConsentProvider = NotifierProvider( @@ -36,21 +32,62 @@ class AiConsentNotifier extends Notifier { Future load(String userId) async { state = AiConsentState(isLoading: true, userId: userId); - final value = await ref.read(localDbProvider).read(_key(userId)); + try { + final value = await ref.read(localDbProvider).read(_key(userId)); + state = AiConsentState( + isLoading: false, + granted: value == 'granted', + userId: userId, + ); + } catch (_) { + state = AiConsentState( + isLoading: false, + userId: userId, + error: 'Authorization status could not be loaded.', + ); + } + } + + Future grant(String userId) async { state = AiConsentState( isLoading: false, - granted: value == 'granted', + isSaving: true, + granted: state.granted, userId: userId, ); + try { + await ref.read(localDbProvider).write(_key(userId), 'granted'); + state = AiConsentState(isLoading: false, granted: true, userId: userId); + return true; + } catch (_) { + state = AiConsentState( + isLoading: false, + userId: userId, + error: 'Authorization could not be saved.', + ); + return false; + } } - Future grant(String userId) async { - await ref.read(localDbProvider).write(_key(userId), 'granted'); - state = AiConsentState(isLoading: false, granted: true, userId: userId); - } - - Future revoke(String userId) async { - await ref.read(localDbProvider).delete(_key(userId)); - state = AiConsentState(isLoading: false, granted: false, userId: userId); + Future revoke(String userId) async { + state = AiConsentState( + isLoading: false, + isSaving: true, + granted: state.granted, + userId: userId, + ); + try { + await ref.read(localDbProvider).delete(_key(userId)); + state = AiConsentState(isLoading: false, granted: false, userId: userId); + return true; + } catch (_) { + state = AiConsentState( + isLoading: false, + granted: true, + userId: userId, + error: 'Authorization could not be revoked.', + ); + return false; + } } } diff --git a/health_app/lib/utils/sse_handler.dart b/health_app/lib/utils/sse_handler.dart index 75b9c6e..4950dca 100644 --- a/health_app/lib/utils/sse_handler.dart +++ b/health_app/lib/utils/sse_handler.dart @@ -63,7 +63,10 @@ class SseHandler { cancelToken: cancelToken, options: Options( responseType: ResponseType.stream, - headers: {'Authorization': 'Bearer $token'}, + headers: { + 'Authorization': 'Bearer $token', + 'X-App-Policy': appPolicyProfile, + }, ), ); diff --git a/health_app/lib/widgets/ai_consent_gate.dart b/health_app/lib/widgets/ai_consent_gate.dart index 4bedfbb..faabc17 100644 --- a/health_app/lib/widgets/ai_consent_gate.dart +++ b/health_app/lib/widgets/ai_consent_gate.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/app_colors.dart'; import '../core/app_design_tokens.dart'; +import '../core/navigation_provider.dart'; import '../providers/ai_consent_provider.dart'; import '../providers/auth_provider.dart'; import '../pages/settings/ai_consent_details_page.dart'; @@ -21,7 +22,9 @@ class _AiConsentGateState extends ConsumerState { @override Widget build(BuildContext context) { final user = ref.watch(authProvider).user; - if (user == null || user.id.isEmpty) return widget.child; + if (user == null || user.id.isEmpty || user.role != 'User') { + return widget.child; + } if (_loadedUserId != user.id) { _loadedUserId = user.id; @@ -31,17 +34,31 @@ class _AiConsentGateState extends ConsumerState { } final consent = ref.watch(aiConsentProvider); + final currentRoute = ref.watch(currentRouteProvider).name; + final isConsentInformationRoute = + currentRoute == 'aiConsentDetails' || currentRoute == 'staticText'; if (consent.userId != user.id || consent.isLoading) { - return const Scaffold(backgroundColor: Colors.white); + return const Scaffold( + body: Center( + child: CircularProgressIndicator(color: AppColors.primary), + ), + ); } - if (consent.granted) return widget.child; + if (consent.granted || isConsentInformationRoute) return widget.child; return Scaffold( - backgroundColor: AppColors.background, - body: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: _ConsentCard(userId: user.id), + body: DecoratedBox( + decoration: const BoxDecoration(gradient: AppColors.bgGradient), + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: _ConsentCard(userId: user.id), + ), + ), + ), ), ), ); @@ -53,7 +70,12 @@ class _ConsentCard extends ConsumerWidget { const _ConsentCard({required this.userId}); Future _grant(BuildContext context, WidgetRef ref) async { - await ref.read(aiConsentProvider.notifier).grant(userId); + final succeeded = await ref.read(aiConsentProvider.notifier).grant(userId); + if (!succeeded && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Authorization could not be saved.')), + ); + } } Future _decline(BuildContext context, WidgetRef ref) async { @@ -62,64 +84,108 @@ class _ConsentCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Card( - elevation: 0, - color: Colors.white, - shape: RoundedRectangleBorder(borderRadius: AppRadius.xlBorder), - child: Padding( - padding: const EdgeInsets.fromLTRB(22, 24, 22, 20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Icon(Icons.auto_awesome, size: 40, color: AppColors.primary), - const SizedBox(height: 14), - const Text( - 'AI 健康服务授权', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 21, fontWeight: FontWeight.w700), + final consent = ref.watch(aiConsentProvider); + return Container( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + borderRadius: AppRadius.cardBorder, + border: Border.all(color: AppColors.primaryLight, width: 1), + boxShadow: AppShadows.panel, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + child: Image.asset( + 'assets/branding/health_login_character_transparent.png', + height: 112, + fit: BoxFit.contain, ), - const SizedBox(height: 14), - const Text( - '小脉健康的核心功能使用第三方 AI 服务。经你同意后,你主动输入的健康信息、对话内容、图片和报告可能会发送给相关 AI 服务,用于健康记录、饮食识别、报告整理和 AI 回复。', - style: TextStyle( - fontSize: 15, - height: 1.6, - color: AppColors.textSecondary, + ), + const SizedBox(height: 2), + Align( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColors.primarySoft, + borderRadius: AppRadius.pillBorder, + ), + child: const Text( + 'AI Service', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), ), ), - const SizedBox(height: 10), - const Text( - 'AI 生成内容仅供健康管理参考,不能替代医生诊断或治疗。', - style: TextStyle( - fontSize: 14, - height: 1.5, - color: AppColors.textSecondary, - ), - ), - const SizedBox(height: 10), - TextButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const AiConsentDetailsPage(), + ), + const SizedBox(height: 14), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon( + Icons.auto_awesome, + size: 40, + color: AppColors.primary, + ), + const SizedBox(height: 14), + const Text( + 'AI 健康服务授权', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 21, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 14), + const Text( + '小脉健康的核心功能使用第三方 AI 服务。经你同意后,你主动输入的健康信息、对话内容、图片和报告可能会发送给相关 AI 服务,用于健康记录、饮食识别、报告整理和 AI 回复。', + style: TextStyle( + fontSize: 15, + height: 1.6, + color: AppColors.textSecondary, ), - ); - }, - child: const Text('查看《AI 数据处理说明》'), + ), + const SizedBox(height: 10), + const Text( + 'AI 生成内容仅供健康管理参考,不能替代医生诊断或治疗。', + style: TextStyle( + fontSize: 14, + height: 1.5, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 10), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const AiConsentDetailsPage(), + ), + ); + }, + child: const Text('查看《AI 数据处理说明》'), + ), + const SizedBox(height: 8), + FilledButton( + onPressed: consent.isSaving + ? null + : () => _grant(context, ref), + child: const Text('同意并继续'), + ), + const SizedBox(height: 8), + OutlinedButton( + onPressed: consent.isSaving + ? null + : () => _decline(context, ref), + child: const Text('不同意并退出'), + ), + ], ), - const SizedBox(height: 8), - FilledButton( - onPressed: () => _grant(context, ref), - child: const Text('同意并继续'), - ), - const SizedBox(height: 8), - OutlinedButton( - onPressed: () => _decline(context, ref), - child: const Text('不同意并退出'), - ), - ], - ), + ), + ], ), ); } diff --git a/health_app/lib/widgets/ble_sync_dialog.dart b/health_app/lib/widgets/ble_sync_dialog.dart deleted file mode 100644 index 3ca90fa..0000000 --- a/health_app/lib/widgets/ble_sync_dialog.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../core/app_colors.dart'; -import '../core/app_design_tokens.dart'; -import '../core/app_module_visuals.dart'; -import '../models/bp_reading.dart'; - -Future showBleSyncDialog( - BuildContext context, { - required BpReading reading, -}) { - return showDialog( - context: context, - barrierDismissible: false, - barrierColor: Colors.black.withValues(alpha: 0.54), - builder: (ctx) => Dialog( - insetPadding: const EdgeInsets.symmetric(horizontal: 28), - backgroundColor: Colors.transparent, - child: Container( - width: double.infinity, - constraints: const BoxConstraints(maxWidth: 380), - padding: const EdgeInsets.fromLTRB(22, 26, 22, 22), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: AppRadius.xlBorder, - boxShadow: AppShadows.floating, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: AppModuleVisuals.device.color, - borderRadius: AppRadius.mdBorder, - ), - child: Icon( - AppModuleVisuals.device.icon, - color: Colors.white, - size: 25, - ), - ), - const SizedBox(height: 14), - Text( - '数据已录入', - textAlign: TextAlign.center, - style: AppTextStyles.summaryTitle.copyWith(fontSize: 22), - ), - const SizedBox(height: 22), - Row( - children: [ - Expanded( - child: _MetricCard( - label: '收缩压', - value: '${reading.systolic}', - unit: 'mmHg', - ), - ), - const SizedBox(width: 14), - Expanded( - child: _MetricCard( - label: '舒张压', - value: '${reading.diastolic}', - unit: 'mmHg', - ), - ), - ], - ), - if (reading.pulse != null) ...[ - const SizedBox(height: 14), - Row( - children: [ - Expanded( - child: _MetricCard( - label: '脉搏', - value: '${reading.pulse}', - unit: 'bpm', - ), - ), - const Expanded(child: SizedBox.shrink()), - ], - ), - ], - const SizedBox(height: 26), - GestureDetector( - onTap: () => Navigator.pop(ctx), - child: Container( - height: 50, - width: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - gradient: AppModuleVisuals.device.gradient, - borderRadius: AppRadius.lgBorder, - boxShadow: AppColors.buttonShadow, - ), - child: Text( - '知道了', - style: AppTextStyles.button.copyWith(color: Colors.white), - ), - ), - ), - ], - ), - ), - ), - ); -} - -class _MetricCard extends StatelessWidget { - final String label; - final String value; - final String unit; - - const _MetricCard({ - required this.label, - required this.value, - required this.unit, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - color: AppModuleVisuals.device.lightColor, - borderRadius: AppRadius.mdBorder, - border: Border.all(color: AppModuleVisuals.device.borderColor), - ), - child: Column( - children: [ - Text( - value, - style: const TextStyle( - fontSize: 36, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - height: 1, - ), - ), - const SizedBox(height: 6), - Text(unit, style: AppTextStyles.tag), - const SizedBox(height: 4), - Text(label, style: AppTextStyles.tag), - ], - ), - ); - } -} diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index adb377e..d6dd56d 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -1,5 +1,3 @@ -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -328,82 +326,55 @@ class _MetricTile extends StatelessWidget { @override Widget build(BuildContext context) { final elderMode = ElderModeScope.enabledOf(context); - final height = elderMode ? 112.0 : 100.0; - return ClipRRect( + return InkWell( + onTap: onTap, borderRadius: BorderRadius.circular(18), - child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - child: Container( - height: height, - padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 7), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.white.withValues(alpha: 0.62), - Colors.white.withValues(alpha: 0.42), - const Color(0xFFD9FCFF).withValues(alpha: 0.34), - ], + child: Container( + height: elderMode ? 108 : 100, + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.22), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Colors.white.withValues(alpha: 0.40)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 32, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + info.value, + maxLines: 1, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.w700, + color: Colors.white, + ), ), - borderRadius: BorderRadius.circular(18), - border: Border.all( - color: Colors.white.withValues(alpha: 0.82), - width: 1.2, - ), - boxShadow: [ - BoxShadow( - color: const Color(0xFF0369A1).withValues(alpha: 0.10), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 32, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - info.value, - maxLines: 1, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 23, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), - ), - ), - if (info.unit != null) - Text( - info.unit!, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xE8FFFFFF), - ), - ), - const SizedBox(height: 2), - Text( - info.label, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), - ], ), ), - ), + if (info.unit != null) + Text( + info.unit!, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xE0FFFFFF), + ), + ), + const SizedBox(height: 2), + Text( + info.label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], ), ), ); @@ -460,12 +431,6 @@ class _NavigationSection extends StatelessWidget { route: 'exercisePlan', colors: AppColors.exerciseGradient.colors, ), - _NavItem( - icon: LucideIcons.bluetooth, - title: '设备', - route: 'devices', - colors: AppColors.deviceGradient.colors, - ), ]; return _LightSection( @@ -742,43 +707,9 @@ class _HealthDashboardBackgroundPainter extends CustomPainter { ..shader = const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFF4FACFE), Color(0xFF00F2FE)], + colors: [Color(0xFF00C6FB), Color(0xFF005BEA)], ).createShader(rect); canvas.drawRect(rect, paint); - - final glow = Paint() - ..shader = - RadialGradient( - colors: [ - Colors.white.withValues(alpha: 0.32), - Colors.white.withValues(alpha: 0), - ], - ).createShader( - Rect.fromCircle( - center: Offset(size.width * 0.12, size.height * 0.05), - radius: size.width * 0.7, - ), - ); - canvas.drawCircle( - Offset(size.width * 0.12, size.height * 0.05), - size.width * 0.7, - glow, - ); - - final highlight = Paint() - ..color = Colors.white.withValues(alpha: 0.20) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.2; - canvas.drawLine( - Offset(size.width * 0.52, -12), - Offset(size.width * 0.16, size.height + 18), - highlight, - ); - canvas.drawLine( - Offset(size.width * 0.96, -10), - Offset(size.width * 0.58, size.height + 22), - highlight, - ); } @override