diff --git a/health_app/lib/pages/auth/login_page.dart b/health_app/lib/pages/auth/login_page.dart index b65c184..e6a743b 100644 --- a/health_app/lib/pages/auth/login_page.dart +++ b/health_app/lib/pages/auth/login_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/app_colors.dart'; -import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/data_providers.dart'; @@ -13,6 +12,9 @@ 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'; + final _phoneCtrl = TextEditingController(); final _codeCtrl = TextEditingController(); final _nameCtrl = TextEditingController(); @@ -245,127 +247,253 @@ class _LoginPageState extends ConsumerState { } return Scaffold( - body: AppBackground( - safeArea: true, - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), - child: Container( - padding: const EdgeInsets.fromLTRB(22, 24, 22, 22), - decoration: BoxDecoration( - gradient: AppColors.softGlassGradient, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.borderLight), - boxShadow: AppColors.cardShadow, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _BrandMark(), - const SizedBox(height: 16), - const Text( - '小脉健康', - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.w900, - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 4), - const Text( - '血管病患者的 AI 健康管理助手', - style: TextStyle(fontSize: 13, color: AppColors.textSecondary), - ), - const SizedBox(height: 8), - Text( - _isLogin ? '登录你的健康账户' : '创建新的健康账户', - style: const TextStyle( - fontSize: 14, - color: AppColors.textSecondary, - ), - ), - const SizedBox(height: 26), - if (_successMsg != null) - _NoticeBanner(text: _successMsg!, success: true), - if (!_isLogin) ...[ - _DoctorSelector( - name: _selectedDoctorName, - onTap: _showDoctorPicker, - ), - const SizedBox(height: 12), - _TextField(_nameCtrl, '姓名', TextInputType.name, 20), - const SizedBox(height: 12), - ], - _TextField(_phoneCtrl, '手机号', TextInputType.phone, 11), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _TextField( - _codeCtrl, - '验证码', - TextInputType.number, - 6, - ), - ), - const SizedBox(width: 10), - _SmsButton( - sending: _sending, - countdown: _countdown, - onTap: (_countdown > 0 || _sending) ? null : _sendSms, - ), - ], - ), - const SizedBox(height: 14), - _Agreement( - agreed: _agreed, - onTap: () => setState(() => _agreed = !_agreed), - ), - if (_error != null) ...[ - const SizedBox(height: 12), - _NoticeBanner(text: _error!, success: false), - ], - const SizedBox(height: 22), - _PrimaryButton( - loading: _loading, - label: _isLogin ? '登录' : '注册', - onTap: _loading ? null : _submit, - ), - const SizedBox(height: 16), - GestureDetector( - onTap: () => setState(() { - _isLogin = !_isLogin; - _error = null; - _successMsg = null; - }), - child: Text( - _isLogin ? '没有账号?去注册' : '已有账号?去登录', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppColors.primary, - ), - ), - ), - ], + backgroundColor: Colors.white, + body: Stack( + children: [ + 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), + ), + ), + ), + SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 22, 20, 24), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 46, + ), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 430), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _LoginHero(isLogin: _isLogin), + const SizedBox(height: 22), + _LoginFormCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_successMsg != null) + _NoticeBanner( + text: _successMsg!, + success: true, + ), + if (!_isLogin) ...[ + _DoctorSelector( + name: _selectedDoctorName, + onTap: _showDoctorPicker, + ), + const SizedBox(height: 12), + _TextField( + _nameCtrl, + '姓名', + TextInputType.name, + 20, + icon: Icons.person_outline_rounded, + ), + const SizedBox(height: 12), + ], + _TextField( + _phoneCtrl, + '手机号', + TextInputType.phone, + 11, + icon: Icons.phone_iphone_rounded, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _TextField( + _codeCtrl, + '验证码', + TextInputType.number, + 6, + icon: Icons.verified_outlined, + ), + ), + const SizedBox(width: 10), + _SmsButton( + sending: _sending, + countdown: _countdown, + onTap: (_countdown > 0 || _sending) + ? null + : _sendSms, + ), + ], + ), + const SizedBox(height: 14), + _Agreement( + agreed: _agreed, + onTap: () => + setState(() => _agreed = !_agreed), + ), + if (_error != null) ...[ + const SizedBox(height: 12), + _NoticeBanner( + text: _error!, + success: false, + ), + ], + const SizedBox(height: 22), + _PrimaryButton( + loading: _loading, + label: _isLogin ? '登录' : '注册', + onTap: _loading ? null : _submit, + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () => setState(() { + _isLogin = !_isLogin; + _error = null; + _successMsg = null; + }), + child: Text( + _isLogin ? '没有账号?去注册' : '已有账号?去登录', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + color: AppColors.primary, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + ], ), ); } } +class _LoginHero extends StatelessWidget { + final bool isLogin; + + const _LoginHero({required this.isLogin}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _BrandMark(), + const SizedBox(height: 16), + const Text( + '小脉健康', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.w900, + color: AppColors.textPrimary, + letterSpacing: 0, + ), + ), + const SizedBox(height: 6), + const Text( + '血管病患者的 AI 健康管理助手', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + height: 1.45, + fontWeight: FontWeight.w700, + 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, + ), + ), + ), + ], + ); + } +} + +class _LoginFormCard extends StatelessWidget { + final Widget child; + + const _LoginFormCard({required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.94), + borderRadius: BorderRadius.circular(22), + border: Border.all(color: AppColors.border, width: 1.1), + boxShadow: [ + BoxShadow( + color: const Color(0xFF475467).withValues(alpha: 0.10), + blurRadius: 30, + offset: const Offset(0, 16), + ), + ], + ), + child: child, + ); + } +} + class _BrandMark extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: 68, - height: 68, + width: 72, + height: 72, decoration: BoxDecoration( gradient: AppColors.primaryGradient, - borderRadius: BorderRadius.circular(22), + borderRadius: BorderRadius.circular(24), boxShadow: AppColors.buttonShadow, ), child: const Icon( @@ -382,15 +510,38 @@ class _TextField extends StatelessWidget { final String hint; final TextInputType type; final int maxLen; - const _TextField(this.ctrl, this.hint, this.type, this.maxLen); + final IconData? icon; + + const _TextField(this.ctrl, this.hint, this.type, this.maxLen, {this.icon}); @override Widget build(BuildContext context) => TextField( controller: ctrl, keyboardType: type, maxLength: maxLen, - style: const TextStyle(fontSize: 15, color: AppColors.textPrimary), - decoration: InputDecoration(hintText: hint, counterText: ''), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + ), + decoration: InputDecoration( + hintText: hint, + counterText: '', + prefixIcon: icon == null + ? null + : Icon(icon, size: 20, color: AppColors.textSecondary), + filled: true, + fillColor: const Color(0xFFF8FAFC), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 15), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: AppColors.border, width: 1.1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: AppColors.primary, width: 1.2), + ), + ), ); } @@ -407,17 +558,24 @@ class _DoctorSelector extends StatelessWidget { height: 50, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.border), + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.border, width: 1.1), ), child: Row( children: [ + const Icon( + Icons.medical_services_outlined, + size: 20, + color: AppColors.textSecondary, + ), + const SizedBox(width: 12), Expanded( child: Text( name ?? '请选择医生', style: TextStyle( fontSize: 15, + fontWeight: FontWeight.w700, color: name != null ? AppColors.textPrimary : AppColors.textHint, @@ -452,10 +610,11 @@ class _SmsButton extends StatelessWidget { height: 50, alignment: Alignment.center, decoration: BoxDecoration( - color: disabled ? AppColors.cardInner : AppColors.primary, - borderRadius: BorderRadius.circular(12), + gradient: disabled ? null : AppColors.doctorGradient, + color: disabled ? AppColors.cardInner : null, + borderRadius: BorderRadius.circular(14), border: Border.all( - color: disabled ? AppColors.border : AppColors.primary, + color: disabled ? AppColors.border : Colors.transparent, ), ), child: Text( @@ -485,12 +644,12 @@ class _Agreement extends StatelessWidget { return GestureDetector( onTap: onTap, child: Row( - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 18, height: 18, - margin: const EdgeInsets.only(right: 6), + margin: const EdgeInsets.only(right: 7, top: 1), decoration: BoxDecoration( color: agreed ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(5), @@ -554,6 +713,11 @@ class _NoticeBanner extends StatelessWidget { decoration: BoxDecoration( color: success ? AppColors.successLight : AppColors.errorLight, borderRadius: BorderRadius.circular(12), + border: Border.all( + color: success + ? AppColors.success.withValues(alpha: 0.16) + : AppColors.error.withValues(alpha: 0.16), + ), ), child: Row( children: [