diff --git a/health_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/health_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 12dba48..bba8e8b 100644 Binary files a/health_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/health_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/health_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/health_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 34773e3..a140988 100644 Binary files a/health_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/health_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/health_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/health_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 6380b82..24aa959 100644 Binary files a/health_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/health_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/health_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/health_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index b18d3d8..2ffd500 100644 Binary files a/health_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/health_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/health_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/health_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 0f3b114..c98ca12 100644 Binary files a/health_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/health_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/health_app/assets/branding/drawer_background_v1.png b/health_app/assets/branding/drawer_background_v1.png new file mode 100644 index 0000000..5d8ab29 Binary files /dev/null and b/health_app/assets/branding/drawer_background_v1.png differ diff --git a/health_app/assets/branding/health_icon_master.png b/health_app/assets/branding/health_icon_master.png index a6c3c62..04df7ff 100644 Binary files a/health_app/assets/branding/health_icon_master.png and b/health_app/assets/branding/health_icon_master.png differ diff --git a/health_app/assets/branding/health_splash_master.png b/health_app/assets/branding/health_splash_master.png index ce75740..bdfd7dc 100644 Binary files a/health_app/assets/branding/health_splash_master.png and b/health_app/assets/branding/health_splash_master.png differ diff --git a/health_app/assets/branding/login_background_v1.png b/health_app/assets/branding/login_background_v1.png new file mode 100644 index 0000000..cc7348b Binary files /dev/null and b/health_app/assets/branding/login_background_v1.png differ diff --git a/health_app/assets/branding/page_calendar_scene.png b/health_app/assets/branding/page_calendar_scene.png deleted file mode 100644 index 96ad568..0000000 Binary files a/health_app/assets/branding/page_calendar_scene.png and /dev/null differ diff --git a/health_app/assets/branding/page_exercise_scene.png b/health_app/assets/branding/page_exercise_scene.png deleted file mode 100644 index 408d9cc..0000000 Binary files a/health_app/assets/branding/page_exercise_scene.png and /dev/null differ diff --git a/health_app/assets/branding/page_health_archive_scene.png b/health_app/assets/branding/page_health_archive_scene.png deleted file mode 100644 index f0b1139..0000000 Binary files a/health_app/assets/branding/page_health_archive_scene.png and /dev/null differ diff --git a/health_app/assets/branding/page_medication_scene.png b/health_app/assets/branding/page_medication_scene.png deleted file mode 100644 index 82fe408..0000000 Binary files a/health_app/assets/branding/page_medication_scene.png and /dev/null differ diff --git a/health_app/assets/branding/page_notifications_scene.png b/health_app/assets/branding/page_notifications_scene.png deleted file mode 100644 index d548feb..0000000 Binary files a/health_app/assets/branding/page_notifications_scene.png and /dev/null differ diff --git a/health_app/assets/branding/page_profile_scene.png b/health_app/assets/branding/page_profile_scene.png deleted file mode 100644 index ea4feab..0000000 Binary files a/health_app/assets/branding/page_profile_scene.png and /dev/null differ diff --git a/health_app/assets/branding/page_report_scene.png b/health_app/assets/branding/page_report_scene.png deleted file mode 100644 index 80466d4..0000000 Binary files a/health_app/assets/branding/page_report_scene.png and /dev/null differ diff --git a/health_app/assets/branding/ui_abstract_header_bg_v1.png b/health_app/assets/branding/ui_abstract_header_bg_v1.png deleted file mode 100644 index 5797b31..0000000 Binary files a/health_app/assets/branding/ui_abstract_header_bg_v1.png and /dev/null differ diff --git a/health_app/assets/branding/ui_corner_accent_v1.png b/health_app/assets/branding/ui_corner_accent_v1.png deleted file mode 100644 index b49cc81..0000000 Binary files a/health_app/assets/branding/ui_corner_accent_v1.png and /dev/null differ diff --git a/health_app/assets/branding/ui_empty_cluster_v1.png b/health_app/assets/branding/ui_empty_cluster_v1.png deleted file mode 100644 index 1a640fb..0000000 Binary files a/health_app/assets/branding/ui_empty_cluster_v1.png and /dev/null differ diff --git a/health_app/assets/branding/ui_neutral_cluster_v1.png b/health_app/assets/branding/ui_neutral_cluster_v1.png deleted file mode 100644 index 658f641..0000000 Binary files a/health_app/assets/branding/ui_neutral_cluster_v1.png and /dev/null differ diff --git a/health_app/assets/images/.gitkeep b/health_app/assets/images/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/health_app/assets/images/.gitkeep @@ -0,0 +1 @@ + diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 83b5f02..fdd5c36 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 991bd02..12d18f7 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index afe1e15..8667f01 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index de5998e..a4f1f35 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 73977de..264cd7d 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 8f6fadf..0953cf6 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index f8a4d1e..98a8bfe 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index afe1e15..8667f01 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 080342f..cc44b62 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index f4fb8e9..ea1e1e2 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index f4fb8e9..ea1e1e2 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 60e7bca..3326188 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c6ff2d0..e625107 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 65983a8..45e294f 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index cff2ab4..b13ecaf 100644 Binary files a/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/health_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/health_app/lib/models/bp_reading.dart b/health_app/lib/models/bp_reading.dart index 855c369..65a460a 100644 --- a/health_app/lib/models/bp_reading.dart +++ b/health_app/lib/models/bp_reading.dart @@ -30,4 +30,16 @@ class BpReading { 'unit': 'mmHg', 'recordedAt': timestamp.toUtc().toIso8601String(), }; + + Map? toHeartRateRecord() { + final value = pulse; + if (value == null || value <= 0) return null; + return { + 'type': 'HeartRate', + 'value': value, + 'source': 'DeviceSync', + 'unit': 'bpm', + 'recordedAt': timestamp.toUtc().toIso8601String(), + }; + } } diff --git a/health_app/lib/pages/auth/login_page.dart b/health_app/lib/pages/auth/login_page.dart index e6a743b..29f821e 100644 --- a/health_app/lib/pages/auth/login_page.dart +++ b/health_app/lib/pages/auth/login_page.dart @@ -12,8 +12,7 @@ class LoginPage extends ConsumerStatefulWidget { } class _LoginPageState extends ConsumerState { - static const _headerBg = 'assets/branding/ui_abstract_header_bg_v1.png'; - static const _cornerAccent = 'assets/branding/ui_corner_accent_v1.png'; + static const _loginBg = 'assets/branding/login_background_v1.png'; final _phoneCtrl = TextEditingController(); final _codeCtrl = TextEditingController(); @@ -66,8 +65,12 @@ class _LoginPageState extends ConsumerState { Future _submit() async { if (!_agreed) { - setState(() => _error = '请阅读并同意服务协议和隐私政策'); - return; + final accepted = await _showAgreementDialog(); + if (!mounted || accepted != true) return; + setState(() { + _agreed = true; + _error = null; + }); } if (_phoneCtrl.text.trim().isEmpty || _codeCtrl.text.trim().isEmpty) { setState(() => _error = '请输入手机号和验证码'); @@ -132,6 +135,124 @@ class _LoginPageState extends ConsumerState { } } + Future _showAgreementDialog() { + 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, 28, 22, 22), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.14), + blurRadius: 34, + offset: const Offset(0, 18), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '服务协议及隐私保护', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 21, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + letterSpacing: 0, + ), + ), + const SizedBox(height: 18), + RichText( + textAlign: TextAlign.center, + text: const TextSpan( + style: TextStyle( + fontSize: 15, + height: 1.55, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary, + ), + children: [ + TextSpan(text: '请阅读并同意'), + TextSpan( + text: '《服务协议》', + style: TextStyle(color: AppColors.primary), + ), + TextSpan(text: '和'), + TextSpan( + text: '《隐私政策》', + style: TextStyle(color: AppColors.primary), + ), + ], + ), + ), + const SizedBox(height: 26), + Row( + children: [ + Expanded( + child: SizedBox( + height: 50, + child: OutlinedButton( + onPressed: () => Navigator.pop(ctx, false), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primary, + side: BorderSide( + color: AppColors.primary.withValues(alpha: 0.62), + width: 1.2, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + child: const Text('不同意'), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => Navigator.pop(ctx, true), + child: Container( + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: AppColors.doctorGradient, + borderRadius: BorderRadius.circular(18), + boxShadow: AppColors.buttonShadow, + ), + child: const Text( + '同意', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + void _goHome() { ref.invalidate(latestHealthProvider); ref.invalidate(medicationListProvider); @@ -250,34 +371,26 @@ class _LoginPageState extends ConsumerState { backgroundColor: Colors.white, body: Stack( children: [ + Positioned.fill( + child: Image.asset( + _loginBg, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + ), Positioned.fill( child: DecoratedBox( - decoration: BoxDecoration(gradient: AppColors.bgGradient), - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - height: 260, - child: Opacity( - opacity: 0.92, - child: Image.asset( - _headerBg, - fit: BoxFit.cover, - alignment: Alignment.topCenter, - ), - ), - ), - Positioned( - top: 18, - right: -44, - width: 180, - height: 180, - child: IgnorePointer( - child: Opacity( - opacity: 0.86, - child: Image.asset(_cornerAccent, fit: BoxFit.contain), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withValues(alpha: 0.10), + Colors.white.withValues(alpha: 0.28), + Colors.white.withValues(alpha: 0.54), + ], + stops: const [0.0, 0.52, 1.0], + ), ), ), ), @@ -285,10 +398,13 @@ class _LoginPageState extends ConsumerState { child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 22, 20, 24), + padding: const EdgeInsets.fromLTRB(20, 28, 20, 24), child: ConstrainedBox( constraints: BoxConstraints( - minHeight: constraints.maxHeight - 46, + minHeight: (constraints.maxHeight - 52).clamp( + 0.0, + double.infinity, + ), ), child: Center( child: ConstrainedBox( @@ -297,8 +413,8 @@ class _LoginPageState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _LoginHero(isLogin: _isLogin), - const SizedBox(height: 22), + const _LoginHero(), + const SizedBox(height: 28), _LoginFormCard( child: Column( mainAxisSize: MainAxisSize.min, @@ -328,7 +444,8 @@ class _LoginPageState extends ConsumerState { '手机号', TextInputType.phone, 11, - icon: Icons.phone_iphone_rounded, + icon: Icons.local_phone_rounded, + gradientIcon: true, ), const SizedBox(height: 12), Row( @@ -339,7 +456,8 @@ class _LoginPageState extends ConsumerState { '验证码', TextInputType.number, 6, - icon: Icons.verified_outlined, + icon: Icons.sms_rounded, + gradientIcon: true, ), ), const SizedBox(width: 10), @@ -407,9 +525,7 @@ class _LoginPageState extends ConsumerState { } class _LoginHero extends StatelessWidget { - final bool isLogin; - - const _LoginHero({required this.isLogin}); + const _LoginHero(); @override Widget build(BuildContext context) { @@ -437,23 +553,6 @@ class _LoginHero extends StatelessWidget { color: AppColors.textSecondary, ), ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.76), - borderRadius: BorderRadius.circular(999), - border: Border.all(color: AppColors.borderLight), - ), - child: Text( - isLogin ? '登录你的健康账户' : '创建新的健康账户', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - ), - ), - ), ], ); } @@ -488,18 +587,15 @@ class _LoginFormCard extends StatelessWidget { class _BrandMark extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - width: 72, - height: 72, - decoration: BoxDecoration( - gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(24), - boxShadow: AppColors.buttonShadow, - ), - child: const Icon( - Icons.monitor_heart_outlined, - color: Colors.white, - size: 34, + return SizedBox( + width: 132, + height: 132, + child: Transform.scale( + scale: 1.08, + child: Image.asset( + 'assets/branding/health_icon_master.png', + fit: BoxFit.cover, + ), ), ); } @@ -511,8 +607,16 @@ class _TextField extends StatelessWidget { final TextInputType type; final int maxLen; final IconData? icon; + final bool gradientIcon; - const _TextField(this.ctrl, this.hint, this.type, this.maxLen, {this.icon}); + const _TextField( + this.ctrl, + this.hint, + this.type, + this.maxLen, { + this.icon, + this.gradientIcon = false, + }); @override Widget build(BuildContext context) => TextField( @@ -529,6 +633,8 @@ class _TextField extends StatelessWidget { counterText: '', prefixIcon: icon == null ? null + : gradientIcon + ? _GradientPrefixIcon(icon: icon!) : Icon(icon, size: 20, color: AppColors.textSecondary), filled: true, fillColor: const Color(0xFFF8FAFC), @@ -545,6 +651,20 @@ class _TextField extends StatelessWidget { ); } +class _GradientPrefixIcon extends StatelessWidget { + final IconData icon; + const _GradientPrefixIcon({required this.icon}); + + @override + Widget build(BuildContext context) { + return ShaderMask( + shaderCallback: AppColors.doctorGradient.createShader, + blendMode: BlendMode.srcIn, + child: Icon(icon, size: 23, color: Colors.white), + ); + } +} + class _DoctorSelector extends StatelessWidget { final String? name; final VoidCallback onTap; @@ -762,7 +882,7 @@ class _PrimaryButton extends StatelessWidget { height: 52, alignment: Alignment.center, decoration: BoxDecoration( - gradient: AppColors.primaryGradient, + gradient: AppColors.doctorGradient, borderRadius: BorderRadius.circular(14), boxShadow: AppColors.buttonShadow, ), diff --git a/health_app/lib/pages/notifications/notification_center_page.dart b/health_app/lib/pages/notifications/notification_center_page.dart index df516bd..84ae408 100644 --- a/health_app/lib/pages/notifications/notification_center_page.dart +++ b/health_app/lib/pages/notifications/notification_center_page.dart @@ -3,7 +3,6 @@ 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'; import '../../providers/data_providers.dart'; import '../../services/in_app_notification_service.dart'; @@ -147,7 +146,7 @@ class _NotificationCenterPageState final unread = _history?.unreadCount ?? 0; return Scaffold( - backgroundColor: const Color(0xFFF8FAFF), + backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, @@ -248,19 +247,19 @@ class _NotificationCenterPageState return ListView( physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), + padding: const EdgeInsets.fromLTRB(16, 14, 16, 32), children: [ if (unread > 0) _UnreadHint(unreadCount: unread), const SizedBox(height: 8), if (today.isNotEmpty) ...[ const _SectionTitle('今天'), - const SizedBox(height: 8), + const SizedBox(height: 10), ...today.map(_buildDismissible), ], if (earlier.isNotEmpty) ...[ - if (today.isNotEmpty) const SizedBox(height: 14), + if (today.isNotEmpty) const SizedBox(height: 18), const _SectionTitle('更早'), - const SizedBox(height: 8), + const SizedBox(height: 10), ...earlier.map(_buildDismissible), ], ], @@ -268,7 +267,7 @@ class _NotificationCenterPageState } Widget _buildDismissible(InAppNotification item) => Padding( - padding: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.only(bottom: 12), child: Dismissible( key: ValueKey(item.id), direction: DismissDirection.endToStart, @@ -281,7 +280,7 @@ class _NotificationCenterPageState end: Alignment.centerRight, colors: [AppColors.error.withValues(alpha: 0.6), AppColors.error], ), - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(28), ), child: const Row( mainAxisSize: MainAxisSize.min, @@ -312,15 +311,21 @@ class _UnreadHint extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + return Container( + margin: const EdgeInsets.fromLTRB(0, 4, 0, 12), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFFFFFBEB), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: const Color(0xFFFDE68A)), + ), child: Row( children: [ Container( - width: 6, - height: 6, + width: 8, + height: 8, decoration: const BoxDecoration( - color: Color(0xFFEF4444), + color: Color(0xFFF97316), shape: BoxShape.circle, ), ), @@ -329,8 +334,8 @@ class _UnreadHint extends StatelessWidget { '你有 $unreadCount 条未读消息', style: const TextStyle( fontSize: 13, - fontWeight: FontWeight.w600, - color: AppColors.textSecondary, + fontWeight: FontWeight.w800, + color: Color(0xFF92400E), ), ), ], @@ -344,15 +349,21 @@ class _SectionTitle extends StatelessWidget { const _SectionTitle(this.text); @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.fromLTRB(2, 12, 2, 6), + Widget build(BuildContext context) => Container( + margin: const EdgeInsets.fromLTRB(0, 12, 0, 2), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), child: Row( children: [ Container( - width: 4, - height: 16, + width: 8, + height: 8, decoration: BoxDecoration( - color: AppColors.primary, + gradient: AppColors.doctorGradient, borderRadius: BorderRadius.circular(99), ), ), @@ -366,8 +377,18 @@ class _SectionTitle extends StatelessWidget { ), ), const SizedBox(width: 10), - const Expanded( - child: Divider(height: 1, thickness: 1, color: AppColors.divider), + Expanded( + child: Container( + height: 1, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFFE5E7EB), + const Color(0xFFE5E7EB).withValues(alpha: 0), + ], + ), + ), + ), ), ], ), @@ -385,26 +406,32 @@ class _NotificationCard extends StatelessWidget { final visual = _NotificationVisual.of(item); return Material( color: Colors.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(28), + elevation: 0, child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(28), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(28), border: Border.all( color: item.isRead - ? AppColors.border - : visual.color.withValues(alpha: 0.34), - width: item.isRead ? 1 : 1.2, + ? const Color(0xFFF1F5F9) + : visual.color.withValues(alpha: 0.24), + width: 1, ), boxShadow: [ BoxShadow( - color: const Color(0xFF101828).withValues(alpha: 0.03), - blurRadius: 8, - offset: const Offset(0, 2), + color: const Color(0xFF0F172A).withValues(alpha: 0.08), + blurRadius: 22, + offset: const Offset(0, 10), + ), + BoxShadow( + color: visual.color.withValues(alpha: item.isRead ? 0 : 0.08), + blurRadius: 18, + offset: const Offset(0, 8), ), ], ), @@ -416,21 +443,24 @@ class _NotificationCard extends StatelessWidget { clipBehavior: Clip.none, children: [ Container( - width: 44, - height: 44, + width: 48, + height: 48, decoration: BoxDecoration( color: visual.lightColor, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: visual.color.withValues(alpha: 0.12), + ), ), - child: Icon(visual.icon, size: 22, color: visual.color), + child: Icon(visual.icon, size: 23, color: visual.color), ), if (!item.isRead) Positioned( top: -2, right: -2, child: Container( - width: 9, - height: 9, + width: 10, + height: 10, decoration: BoxDecoration( color: const Color(0xFFEF4444), shape: BoxShape.circle, @@ -440,7 +470,7 @@ class _NotificationCard extends StatelessWidget { ), ], ), - const SizedBox(width: 12), + const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -463,25 +493,36 @@ class _NotificationCard extends StatelessWidget { ), ), ), - const SizedBox(width: 6), - Text( - _formatTime(item.createdAt), - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.textSecondary, + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + _formatTime(item.createdAt), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary, + ), ), ), ], ), - const SizedBox(height: 5), + const SizedBox(height: 6), Text( item.message, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, - height: 1.45, + height: 1.5, + fontWeight: FontWeight.w500, color: AppColors.textSecondary, ), ), diff --git a/health_app/lib/pages/remaining_pages.dart b/health_app/lib/pages/remaining_pages.dart index 31766ef..3b9866c 100644 --- a/health_app/lib/pages/remaining_pages.dart +++ b/health_app/lib/pages/remaining_pages.dart @@ -2885,6 +2885,44 @@ class StaticTextPage extends ConsumerWidget { ### 六、联系我们 如有任何关于隐私的问题,请联系: 邮箱:privacy@healthbutler.com +电话:400-xxx-xxxx''', + 'terms': '''## 服务协议 + +更新日期:2026年6月26日 + +欢迎使用小脉健康。请您在注册、登录和使用本应用前,仔细阅读并理解本服务协议。您点击同意或继续使用本应用,即表示您已阅读、理解并同意本协议。 + +### 一、服务内容 +小脉健康为用户提供健康数据记录、用药管理、报告管理、运动计划、健康日历、AI 健康建议及在线医生咨询等功能。具体服务内容会根据产品版本和实际开通情况进行调整。 + +### 二、账号使用 +- 您应使用真实、准确、有效的信息完成注册和登录。 +- 您应妥善保管账号、验证码及设备信息,不得将账号转让、出租或出借给他人使用。 +- 因您主动泄露账号信息或操作不当造成的损失,由您自行承担。 + +### 三、健康服务说明 +- 本应用提供的 AI 分析、健康提醒和运动饮食建议仅作为健康管理参考,不替代医生面对面的诊断、治疗或处方。 +- 如您出现胸痛、呼吸困难、意识异常、严重头痛等紧急情况,应立即拨打急救电话或前往医疗机构就诊。 +- 医生咨询、报告解读等内容应结合您的实际病史、检查结果和线下诊疗意见综合判断。 + +### 四、用户行为规范 +您在使用本应用时不得: +- 提交虚假、违法、侵权或误导性信息; +- 干扰应用正常运行,或尝试未经授权访问系统、数据和接口; +- 利用本应用从事违法违规、损害他人权益或扰乱医疗服务秩序的行为。 + +### 五、数据与隐私 +我们会按照《隐私政策》收集、使用、存储和保护您的个人信息及健康数据。您可在 App 内「设置」中查看隐私政策,并依法行使查询、更正、删除和注销账号等权利。 + +### 六、服务变更与中止 +为提升服务质量或满足合规要求,我们可能对功能、页面、规则或服务范围进行调整。若因维护、升级、网络故障、不可抗力或法律法规要求导致服务暂时中断,我们会尽力降低对您的影响。 + +### 七、责任限制 +在法律允许范围内,小脉健康不对因第三方服务、网络故障、设备异常、用户误操作或不可抗力造成的服务中断、数据延迟或损失承担超出法定范围的责任。 + +### 八、联系我们 +如您对本协议或服务使用有任何疑问,请通过以下方式联系我们: +邮箱:support@healthbutler.com 电话:400-xxx-xxxx''', 'about': '''## 关于小脉健康 @@ -2914,56 +2952,76 @@ class StaticTextPage extends ConsumerWidget { © 2025-2026 小脉健康团队。保留所有权利。 本软件受中华人民共和国著作权法保护。''', }; - return GradientScaffold( + return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, + scrolledUnderElevation: 0.5, + surfaceTintColor: Colors.transparent, leading: IconButton( - icon: const Icon(Icons.chevron_left), + icon: const Icon(Icons.chevron_left, color: AppColors.textPrimary), onPressed: () => popRoute(ref), ), title: Text( titles[type] ?? '', - style: TextStyle( + style: const TextStyle( + fontSize: 19, color: AppColors.textPrimary, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w800, ), ), centerTitle: true, ), body: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: MarkdownBody( - data: contents[type] ?? '内容加载中...', - selectable: true, - styleSheet: MarkdownStyleSheet( - p: const TextStyle( - fontSize: 17, - height: 1.8, - color: AppColors.textPrimary, - ), - h1: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - height: 1.5, - ), - h2: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - height: 1.5, - ), - h3: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: AppColors.textPrimary, - height: 1.5, - ), - listBullet: const TextStyle( - fontSize: 17, - height: 1.8, - color: AppColors.textPrimary, + padding: const EdgeInsets.fromLTRB(22, 18, 22, 36), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 680), + child: MarkdownBody( + data: contents[type] ?? '内容加载中...', + selectable: true, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.start, + p: const TextStyle( + fontSize: 15, + height: 1.85, + fontWeight: FontWeight.w500, + color: AppColors.textPrimary, + ), + h1Align: WrapAlignment.center, + h1Padding: const EdgeInsets.only(bottom: 18), + h1: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + height: 1.35, + letterSpacing: 0, + ), + h2Padding: const EdgeInsets.only(top: 18, bottom: 8), + h2: const TextStyle( + fontSize: 19, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + height: 1.45, + letterSpacing: 0, + ), + h3Padding: const EdgeInsets.only(top: 16, bottom: 6), + h3: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + height: 1.45, + letterSpacing: 0, + ), + listBullet: const TextStyle( + fontSize: 15, + height: 1.85, + color: AppColors.textPrimary, + ), + blockSpacing: 10, + listIndent: 22, + ), ), ), ), diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index 7fa3c37..c7a4421 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -19,10 +19,7 @@ class SettingsPage extends ConsumerWidget { scrolledUnderElevation: 0.5, surfaceTintColor: Colors.transparent, leading: IconButton( - icon: const Icon( - Icons.arrow_back, - color: AppColors.textPrimary, - ), + icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () => popRoute(ref), ), title: const Text( @@ -44,21 +41,18 @@ class SettingsPage extends ConsumerWidget { _SettingsTile( icon: LucideIcons.bluetooth, title: '蓝牙设备', - colors: const [Color(0xFF38BDF8), Color(0xFF2563EB)], onTap: () => pushRoute(ref, 'devices'), ), const SizedBox(height: 8), _SettingsTile( icon: LucideIcons.bell, title: '消息通知', - colors: const [Color(0xFFA78BFA), Color(0xFF7C3AED)], onTap: () => pushRoute(ref, 'notificationPrefs'), ), const SizedBox(height: 8), _SettingsTile( icon: LucideIcons.info, title: '关于小脉健康', - colors: const [Color(0xFF60A5FA), Color(0xFF0891B2)], onTap: () => pushRoute(ref, 'staticText', params: {'type': 'about'}), ), @@ -66,12 +60,15 @@ class SettingsPage extends ConsumerWidget { _SettingsTile( icon: LucideIcons.shield, title: '隐私协议', - colors: const [Color(0xFFF472B6), Color(0xFFDB2777)], - onTap: () => pushRoute( - ref, - 'staticText', - params: {'type': 'privacy'}, - ), + onTap: () => + pushRoute(ref, 'staticText', params: {'type': 'privacy'}), + ), + const SizedBox(height: 8), + _SettingsTile( + icon: Icons.description_outlined, + title: '服务协议', + onTap: () => + pushRoute(ref, 'staticText', params: {'type': 'terms'}), ), const SizedBox(height: 24), OutlinedButton.icon( @@ -80,11 +77,18 @@ class SettingsPage extends ConsumerWidget { label: const Text('删除账号'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.error, - side: BorderSide(color: AppColors.error.withValues(alpha: 0.34)), + side: BorderSide( + color: AppColors.error.withValues(alpha: 0.34), + ), backgroundColor: Colors.white, minimumSize: const Size.fromHeight(52), - textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), ), ), const SizedBox(height: 12), @@ -96,7 +100,10 @@ class SettingsPage extends ConsumerWidget { backgroundColor: AppColors.error, foregroundColor: Colors.white, minimumSize: const Size.fromHeight(52), - textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -113,12 +120,20 @@ class SettingsPage extends ConsumerWidget { 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('删除账号', 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))), + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('确认删除', style: TextStyle(color: AppColors.error)), + ), ], ), ); @@ -160,15 +175,11 @@ class SettingsPage extends ConsumerWidget { 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 @@ -188,32 +199,16 @@ class _SettingsTile extends StatelessWidget { ), child: Row( children: [ - Icon(icon, color: const Color(0xFF64748B), size: 22), + Icon(icon, color: Colors.black, size: 22), const SizedBox(width: 14), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.textPrimary, - ), - ), - if (subtitle != null) ...[ - const SizedBox(height: 3), - Text( - subtitle!, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: AppColors.textHint, - ), - ), - ], - ], + child: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), ), ), const Icon( diff --git a/health_app/lib/widgets/admin_drawer.dart b/health_app/lib/widgets/admin_drawer.dart index b957720..3a10e3f 100644 --- a/health_app/lib/widgets/admin_drawer.dart +++ b/health_app/lib/widgets/admin_drawer.dart @@ -4,6 +4,7 @@ import '../core/app_colors.dart'; import '../core/navigation_provider.dart'; import '../pages/admin/admin_home_page.dart' show adminPageProvider; import '../providers/auth_provider.dart'; +import 'drawer_shell.dart'; class AdminDrawer extends ConsumerWidget { const AdminDrawer({super.key}); @@ -12,100 +13,99 @@ class AdminDrawer extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentPage = ref.watch(adminPageProvider); - return Drawer( - width: MediaQuery.of(context).size.width * 0.8, - child: Container( - decoration: const BoxDecoration(gradient: AppColors.drawerGradient), - child: SafeArea( - child: Column( - children: [ - Container( - margin: const EdgeInsets.all(14), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: AppColors.softGlassGradient, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.borderLight), - boxShadow: AppColors.cardShadow, + return DrawerShell( + child: SafeArea( + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(14), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.88), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Colors.white.withValues(alpha: 0.86), + width: 1.2, ), - child: Row( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(18), - boxShadow: AppColors.buttonShadow, - ), - child: const Icon( - Icons.admin_panel_settings, - size: 28, - color: Colors.white, - ), + boxShadow: AppColors.cardShadowLight, + ), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: AppColors.doctorGradient, + borderRadius: BorderRadius.circular(20), + boxShadow: AppColors.buttonShadow, ), - const SizedBox(width: 12), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '系统管理员', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - ), - ), - SizedBox(height: 3), - Text( - '医生与患者管理', - style: TextStyle( - fontSize: 12, - color: AppColors.textSecondary, - ), - ), - ], - ), + child: const Icon( + Icons.admin_panel_settings, + size: 28, + color: Colors.white, ), - ], - ), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '系统管理员', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w800, + color: AppColors.textPrimary, + ), + ), + SizedBox(height: 3), + Text( + '医生与患者管理', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + ], ), - const SizedBox(height: 8), - _MenuItem( - icon: Icons.local_hospital, - label: '医生管理', - selected: currentPage == 'doctors', - onTap: () { - Navigator.pop(context); - ref.read(adminPageProvider.notifier).set('doctors'); - }, - ), - _MenuItem( - icon: Icons.people_outline, - label: '患者列表', - selected: currentPage == 'patients', - onTap: () { - Navigator.pop(context); - ref.read(adminPageProvider.notifier).set('patients'); - }, - ), - const Spacer(), - const Divider(height: 1, color: AppColors.divider), - const SizedBox(height: 8), - _MenuItem( - icon: Icons.logout, - label: '退出登录', - danger: true, - onTap: () async { - Navigator.pop(context); - await ref.read(authProvider.notifier).logout(); - goRoute(ref, 'login'); - }, - ), - const SizedBox(height: 16), - ], - ), + ), + const SizedBox(height: 8), + _MenuItem( + icon: Icons.local_hospital, + label: '医生管理', + selected: currentPage == 'doctors', + onTap: () { + Navigator.pop(context); + ref.read(adminPageProvider.notifier).set('doctors'); + }, + ), + _MenuItem( + icon: Icons.people_outline, + label: '患者列表', + selected: currentPage == 'patients', + onTap: () { + Navigator.pop(context); + ref.read(adminPageProvider.notifier).set('patients'); + }, + ), + const Spacer(), + const Divider(height: 1, color: AppColors.divider), + const SizedBox(height: 8), + _MenuItem( + icon: Icons.logout, + label: '退出登录', + danger: true, + onTap: () async { + Navigator.pop(context); + await ref.read(authProvider.notifier).logout(); + goRoute(ref, 'login'); + }, + ), + const SizedBox(height: 16), + ], ), ), ); @@ -131,7 +131,7 @@ class _MenuItem extends StatelessWidget { final color = danger ? AppColors.error : selected - ? AppColors.primary + ? const Color(0xFF7C5CFF) : AppColors.textPrimary; return Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), @@ -145,13 +145,14 @@ class _MenuItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( gradient: selected - ? AppColors.primaryGradient + ? AppColors.doctorGradient : AppColors.surfaceGradient, borderRadius: BorderRadius.circular(14), border: Border.all( color: selected - ? AppColors.primaryLight + ? Colors.white.withValues(alpha: 0.80) : AppColors.borderLight, + width: 1.1, ), boxShadow: selected ? AppColors.buttonShadow diff --git a/health_app/lib/widgets/doctor_drawer.dart b/health_app/lib/widgets/doctor_drawer.dart index 877e34c..90c88f9 100644 --- a/health_app/lib/widgets/doctor_drawer.dart +++ b/health_app/lib/widgets/doctor_drawer.dart @@ -4,6 +4,7 @@ import '../core/app_colors.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../pages/doctor/doctor_home_page.dart' show doctorPageProvider; +import 'drawer_shell.dart'; class DoctorDrawer extends ConsumerWidget { const DoctorDrawer({super.key}); @@ -14,94 +15,90 @@ class DoctorDrawer extends ConsumerWidget { final currentPage = ref.watch(doctorPageProvider); final name = auth.user?.name ?? '医生'; - return Drawer( - width: MediaQuery.of(context).size.width * 0.8, - child: Container( - decoration: const BoxDecoration(gradient: AppColors.drawerGradient), - child: SafeArea( - child: Column( - children: [ - _DrawerHeader( - name: name, - subtitle: '点击完善医生信息', - icon: Icons.medical_services_outlined, - onTap: () { - Navigator.pop(context); - pushRoute(ref, 'doctorProfile'); - }, - ), - const SizedBox(height: 8), - _DrawerItem( - icon: Icons.dashboard_outlined, - label: '工作台', - selected: currentPage == 'dashboard', - onTap: () { - Navigator.pop(context); - ref.read(doctorPageProvider.notifier).set('dashboard'); - }, - ), - _DrawerItem( - icon: Icons.people_outline, - label: '患者管理', - selected: currentPage == 'patients', - onTap: () { - Navigator.pop(context); - ref.read(doctorPageProvider.notifier).set('patients'); - }, - ), - _DrawerItem( - icon: Icons.chat_outlined, - label: '问诊列表', - selected: currentPage == 'consultations', - onTap: () { - Navigator.pop(context); - ref.read(doctorPageProvider.notifier).set('consultations'); - }, - ), - _DrawerItem( - icon: Icons.description_outlined, - label: '报告审核', - selected: currentPage == 'reports', - onTap: () { - Navigator.pop(context); - ref.read(doctorPageProvider.notifier).set('reports'); - }, - ), - _DrawerItem( - icon: Icons.event_note_outlined, - label: '复查随访', - selected: currentPage == 'followups', - onTap: () { - Navigator.pop(context); - ref.read(doctorPageProvider.notifier).set('followups'); - }, - ), - const Spacer(), - const Divider(height: 1, color: AppColors.divider), - const SizedBox(height: 8), - _DrawerItem( - icon: Icons.settings_outlined, - label: '设置', - selected: false, - onTap: () { - Navigator.pop(context); - pushRoute(ref, 'doctorSettings'); - }, - ), - _DrawerItem( - icon: Icons.logout, - label: '退出登录', - selected: false, - danger: true, - onTap: () async { - Navigator.pop(context); - await ref.read(authProvider.notifier).logout(); - goRoute(ref, 'login'); - }, - ), - const SizedBox(height: 16), - ], - ), + return DrawerShell( + child: SafeArea( + child: Column( + children: [ + _DrawerHeader( + name: name, + subtitle: '点击完善医生信息', + icon: Icons.medical_services_outlined, + onTap: () { + Navigator.pop(context); + pushRoute(ref, 'doctorProfile'); + }, + ), + const SizedBox(height: 8), + _DrawerItem( + icon: Icons.dashboard_outlined, + label: '工作台', + selected: currentPage == 'dashboard', + onTap: () { + Navigator.pop(context); + ref.read(doctorPageProvider.notifier).set('dashboard'); + }, + ), + _DrawerItem( + icon: Icons.people_outline, + label: '患者管理', + selected: currentPage == 'patients', + onTap: () { + Navigator.pop(context); + ref.read(doctorPageProvider.notifier).set('patients'); + }, + ), + _DrawerItem( + icon: Icons.chat_outlined, + label: '问诊列表', + selected: currentPage == 'consultations', + onTap: () { + Navigator.pop(context); + ref.read(doctorPageProvider.notifier).set('consultations'); + }, + ), + _DrawerItem( + icon: Icons.description_outlined, + label: '报告审核', + selected: currentPage == 'reports', + onTap: () { + Navigator.pop(context); + ref.read(doctorPageProvider.notifier).set('reports'); + }, + ), + _DrawerItem( + icon: Icons.event_note_outlined, + label: '复查随访', + selected: currentPage == 'followups', + onTap: () { + Navigator.pop(context); + ref.read(doctorPageProvider.notifier).set('followups'); + }, + ), + const Spacer(), + const Divider(height: 1, color: AppColors.divider), + const SizedBox(height: 8), + _DrawerItem( + icon: Icons.settings_outlined, + label: '设置', + selected: false, + onTap: () { + Navigator.pop(context); + pushRoute(ref, 'doctorSettings'); + }, + ), + _DrawerItem( + icon: Icons.logout, + label: '退出登录', + selected: false, + danger: true, + onTap: () async { + Navigator.pop(context); + await ref.read(authProvider.notifier).logout(); + goRoute(ref, 'login'); + }, + ), + const SizedBox(height: 16), + ], ), ), ); @@ -126,10 +123,13 @@ class _DrawerHeader extends StatelessWidget { margin: const EdgeInsets.all(14), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - gradient: AppColors.softGlassGradient, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.borderLight), - boxShadow: AppColors.cardShadow, + color: Colors.white.withValues(alpha: 0.88), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Colors.white.withValues(alpha: 0.86), + width: 1.2, + ), + boxShadow: AppColors.cardShadowLight, ), child: InkWell( onTap: onTap, @@ -137,11 +137,11 @@ class _DrawerHeader extends StatelessWidget { child: Row( children: [ Container( - width: 52, - height: 52, + width: 56, + height: 56, decoration: BoxDecoration( gradient: AppColors.doctorGradient, - borderRadius: BorderRadius.circular(18), + borderRadius: BorderRadius.circular(20), boxShadow: AppColors.buttonShadow, ), child: Icon(icon, color: Colors.white, size: 26), @@ -198,7 +198,7 @@ class _DrawerItem extends StatelessWidget { final color = danger ? AppColors.error : selected - ? AppColors.primary + ? const Color(0xFF7C5CFF) : AppColors.textPrimary; return Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), @@ -212,13 +212,14 @@ class _DrawerItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( gradient: selected - ? AppColors.primaryGradient + ? AppColors.doctorGradient : AppColors.surfaceGradient, borderRadius: BorderRadius.circular(14), border: Border.all( color: selected - ? AppColors.primaryLight + ? Colors.white.withValues(alpha: 0.80) : AppColors.borderLight, + width: 1.1, ), boxShadow: selected ? AppColors.buttonShadow diff --git a/health_app/lib/widgets/drawer_shell.dart b/health_app/lib/widgets/drawer_shell.dart new file mode 100644 index 0000000..0b21876 --- /dev/null +++ b/health_app/lib/widgets/drawer_shell.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class DrawerShell extends StatelessWidget { + final double widthFactor; + final Widget child; + + const DrawerShell({super.key, required this.child, this.widthFactor = 0.84}); + + @override + Widget build(BuildContext context) { + return Drawer( + width: MediaQuery.of(context).size.width * widthFactor, + backgroundColor: Colors.transparent, + elevation: 0, + child: ClipRRect( + borderRadius: const BorderRadius.horizontal(right: Radius.circular(28)), + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + 'assets/branding/drawer_background_v1.png', + fit: BoxFit.cover, + alignment: Alignment.centerLeft, + ), + ), + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withValues(alpha: 0.12), + Colors.white.withValues(alpha: 0.42), + Colors.white.withValues(alpha: 0.72), + ], + stops: const [0.0, 0.48, 1.0], + ), + ), + ), + ), + child, + ], + ), + ), + ); + } +} diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index ed0bef5..d6592a0 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -4,6 +4,7 @@ import '../core/app_colors.dart'; import '../core/navigation_provider.dart'; import '../providers/auth_provider.dart'; import '../providers/data_providers.dart'; +import 'drawer_shell.dart'; class HealthDrawer extends ConsumerWidget { const HealthDrawer({super.key}); @@ -13,37 +14,25 @@ class HealthDrawer extends ConsumerWidget { final auth = ref.watch(authProvider); final latestHealth = ref.watch(latestHealthProvider); - return Drawer( - width: MediaQuery.of(context).size.width * 0.9, - backgroundColor: Colors.transparent, - elevation: 0, - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF87CEEB), Color(0xFFFFFFFF)], - stops: [0.0, 0.35], - ), - ), - child: SafeArea( - child: CustomScrollView( - physics: const BouncingScrollPhysics(), - slivers: [ - SliverPadding( - padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), - sliver: SliverList.list( - children: [ - _AccountHeader(user: auth.user, ref: ref), - const SizedBox(height: 18), - _HealthDashboard(latestHealth: latestHealth, ref: ref), - const SizedBox(height: 18), - _NavigationSection(ref: ref), - ], - ), + return DrawerShell( + widthFactor: 0.9, + child: SafeArea( + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), + sliver: SliverList.list( + children: [ + _AccountHeader(user: auth.user, ref: ref), + const SizedBox(height: 18), + _HealthDashboard(latestHealth: latestHealth, ref: ref), + const SizedBox(height: 18), + _NavigationSection(ref: ref), + ], ), - ], - ), + ), + ], ), ), ); @@ -75,12 +64,12 @@ class _AccountHeader extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, - border: Border.all(color: const Color(0xFFE9EEF5)), + border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( - color: const Color(0xFF101828).withValues(alpha: 0.06), - blurRadius: 14, - offset: const Offset(0, 6), + color: const Color(0xFF7C5CFF).withValues(alpha: 0.16), + blurRadius: 20, + offset: const Offset(0, 10), ), ], ), @@ -152,9 +141,9 @@ class _HealthDashboard extends StatelessWidget { ), titleColor: Colors.white, backgroundGradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF89F7FE), Color(0xFF66A6FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6), Color(0xFFF472B6)], ), child: latestHealth.when( data: (data) => _DashboardMetrics(data: data, ref: ref), @@ -282,7 +271,11 @@ class _MetricTile extends StatelessWidget { if (info.unit != null) Text( info.unit!, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xE0FFFFFF)), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xE0FFFFFF), + ), ), const SizedBox(height: 2), Text( @@ -311,7 +304,7 @@ class _NavigationSection extends StatelessWidget { icon: Icons.folder_shared_rounded, title: '档案', route: 'healthArchive', - colors: const [Color(0xFF6EE7B7), Color(0xFF14B8A6)], + colors: const [Color(0xFF93C5FD), Color(0xFF60A5FA)], ), _NavItem( icon: Icons.description_rounded, @@ -329,13 +322,13 @@ class _NavigationSection extends StatelessWidget { icon: Icons.restaurant_rounded, title: '饮食', route: 'dietRecords', - colors: const [Color(0xFFFED7AA), Color(0xFFFDBA74)], + colors: const [Color(0xFFFBCFE8), Color(0xFFF472B6)], ), _NavItem( icon: Icons.calendar_month_rounded, title: '日历', route: 'calendar', - colors: const [Color(0xFFA7F3D0), Color(0xFF86EFAC)], + colors: const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)], ), _NavItem( icon: Icons.event_available_rounded, @@ -354,9 +347,9 @@ class _NavigationSection extends StatelessWidget { return _Panel( title: '功能入口', backgroundGradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFFEAF4FB), Color(0xFFF4F9FD)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFAFFFFFF), Color(0xF5EFF6FF)], ), child: GridView.builder( itemCount: items.length, @@ -394,13 +387,13 @@ class _NavTile extends StatelessWidget { }; List get _colors => switch (item.route) { - 'healthArchive' => const [Color(0xFF34D399), Color(0xFF059669)], + 'healthArchive' => const [Color(0xFF93C5FD), Color(0xFF60A5FA)], 'reports' => const [Color(0xFF60A5FA), Color(0xFF2563EB)], 'medications' => const [Color(0xFFA78BFA), Color(0xFF7C3AED)], - 'dietRecords' => const [Color(0xFFF6D365), Color(0xFFF97316)], - 'calendar' => const [Color(0xFF34D399), Color(0xFF059669)], + 'dietRecords' => const [Color(0xFFFBCFE8), Color(0xFFF472B6)], + 'calendar' => const [Color(0xFFC4B5FD), Color(0xFF8B5CF6)], 'followups' => const [Color(0xFFF472B6), Color(0xFFDB2777)], - 'exercisePlan' => const [Color(0xFF22D3EE), Color(0xFF0891B2)], + 'exercisePlan' => const [Color(0xFF7DD3FC), Color(0xFF60A5FA)], _ => item.colors, }; @@ -413,7 +406,8 @@ class _NavTile extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white, width: 1.2), + border: Border.all(color: AppColors.borderLight, width: 1.1), + boxShadow: AppColors.cardShadowLight, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -479,7 +473,10 @@ class _Panel extends StatelessWidget { ], ), borderRadius: BorderRadius.circular(28), - border: Border.all(color: Colors.white, width: 1.4), + border: Border.all( + color: Colors.white.withValues(alpha: 0.82), + width: 1.2, + ), boxShadow: [ BoxShadow( color: const Color(0xFF6D5DF6).withValues(alpha: 0.08), diff --git a/health_app/test/widget_test.dart b/health_app/test/widget_test.dart deleted file mode 100644 index f4d2d61..0000000 --- a/health_app/test/widget_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:health_app/core/app_theme.dart'; -import 'package:health_app/pages/auth/login_page.dart'; - -void main() { - testWidgets('主题颜色正确', (tester) async { - expect(AppTheme.primary, const Color(0xFF6366F1)); - expect(AppTheme.primaryLight, const Color(0xFFEEF2FF)); - expect(AppTheme.bg, AppTheme.bg); - expect(AppTheme.error, const Color(0xFFF56C6C)); - expect(AppTheme.success, const Color(0xFF6ECF8A)); - }); - - testWidgets('登录页渲染正常', (tester) async { - await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage()))); - await tester.pumpAndSettle(); - - expect(find.text('小脉健康'), findsOneWidget); - expect(find.text('登 录'), findsOneWidget); - }); - - testWidgets('获取验证码按钮存在', (tester) async { - await tester.pumpWidget(const ProviderScope(child: MaterialApp(home: LoginPage()))); - await tester.pumpAndSettle(); - - expect(find.text('获取验证码'), findsOneWidget); - }); - - testWidgets('AI 气泡样式', (tester) async { - await tester.pumpWidget(MaterialApp( - theme: AppTheme.lightTheme, - home: Scaffold(body: Align( - alignment: Alignment.centerLeft, - child: Container( - constraints: const BoxConstraints(maxWidth: 300), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: const Border(left: BorderSide(color: AppTheme.primaryLight, width: 3)), - ), - child: const Text('收到!已记录', style: TextStyle(fontSize: 16)), - ), - )), - )); - await tester.pumpAndSettle(); - expect(find.text('收到!已记录'), findsOneWidget); - }); - - testWidgets('用户气泡样式', (tester) async { - await tester.pumpWidget(MaterialApp( - theme: AppTheme.lightTheme, - home: Scaffold(body: Align( - alignment: Alignment.centerRight, - child: Container( - constraints: const BoxConstraints(maxWidth: 300), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.primaryLight, - borderRadius: BorderRadius.circular(16), - ), - child: const Text('血压 135/85', style: TextStyle(fontSize: 16, color: Colors.white)), - ), - )), - )); - await tester.pumpAndSettle(); - expect(find.text('血压 135/85'), findsOneWidget); - }); -}