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'; class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState { final _phoneCtrl = TextEditingController(); final _codeCtrl = TextEditingController(); final _nameCtrl = TextEditingController(); String? _selectedDoctorId; String? _selectedDoctorName; bool _agreed = false; bool _sending = false; int _countdown = 0; bool _loading = false; String? _error; bool _isLogin = true; String? _successMsg; @override void dispose() { _phoneCtrl.dispose(); _codeCtrl.dispose(); _nameCtrl.dispose(); super.dispose(); } Future _sendSms() async { final phone = _phoneCtrl.text.trim(); if (phone.length != 11 || !phone.startsWith('1')) { setState(() => _error = '请输入正确的手机号'); return; } setState(() { _sending = true; _error = null; }); final result = await ref.read(authProvider.notifier).sendSms(phone); setState(() => _sending = false); if (result.error != null) { setState(() => _error = result.error); return; } if (result.devCode != null) _codeCtrl.text = result.devCode!; setState(() => _countdown = 60); _startCountdown(); } void _startCountdown() async { for (var i = 60; i > 0; i--) { await Future.delayed(const Duration(seconds: 1)); if (mounted) setState(() => _countdown = i - 1); } } Future _submit() async { if (!_agreed) { setState(() => _error = '请阅读并同意服务协议和隐私政策'); return; } if (_phoneCtrl.text.trim().isEmpty || _codeCtrl.text.trim().isEmpty) { setState(() => _error = '请输入手机号和验证码'); return; } setState(() { _loading = true; _error = null; _successMsg = null; }); String? err; if (_isLogin) { err = await ref .read(authProvider.notifier) .login(_phoneCtrl.text.trim(), _codeCtrl.text.trim()); } else { if (_selectedDoctorId == null) { setState(() { _loading = false; _error = '请选择医生'; }); return; } if (_nameCtrl.text.trim().isEmpty) { setState(() { _loading = false; _error = '请输入姓名'; }); return; } err = await ref .read(authProvider.notifier) .register( _phoneCtrl.text.trim(), _codeCtrl.text.trim(), _nameCtrl.text.trim(), _selectedDoctorId!, ); } setState(() => _loading = false); if (err != null) { if (err.contains('未注册')) setState(() => _isLogin = false); setState(() => _error = err); return; } if (!_isLogin) { setState(() { _isLogin = true; _successMsg = '注册成功,请登录'; _error = null; _phoneCtrl.clear(); _codeCtrl.clear(); _nameCtrl.clear(); _selectedDoctorId = null; _selectedDoctorName = null; }); ref.read(authProvider.notifier).logout(); } else { _goHome(); } } void _goHome() { ref.invalidate(latestHealthProvider); ref.invalidate(medicationListProvider); ref.invalidate(medicationReminderProvider); ref.invalidate(currentExercisePlanProvider); final role = ref.read(authProvider).user?.role ?? 'User'; if (role == 'Admin') { goRoute(ref, 'adminHome'); } else if (role == 'Doctor') { goRoute(ref, 'doctorHome'); } else { goRoute(ref, 'home'); } } void _showDoctorPicker() { showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => Consumer( builder: (ctx, ref, _) { final doctorsAsync = ref.watch(doctorListProvider); return doctorsAsync.when( data: (doctors) { final active = doctors .where((d) => d['isActive'] != false) .toList(); if (active.isEmpty) { return const SizedBox( height: 200, child: Center(child: Text('暂无可用医生')), ); } return SizedBox( height: 420, child: Column( children: [ const Padding( padding: EdgeInsets.all(18), child: Text( '请选择医生', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), ), const Divider(height: 1, color: AppColors.divider), Expanded( child: ListView.builder( itemCount: active.length, itemBuilder: (_, i) { final d = active[i]; final name = d['name'] ?? ''; final title = d['title'] ?? ''; final dept = d['department'] ?? ''; return ListTile( title: Text( name, style: const TextStyle( fontWeight: FontWeight.w700, ), ), subtitle: Text( '$title · $dept', style: const TextStyle( fontSize: 13, color: AppColors.textSecondary, ), ), trailing: _selectedDoctorId == d['id']?.toString() ? const Icon( Icons.check_circle, color: AppColors.primary, ) : null, onTap: () { setState(() { _selectedDoctorId = d['id']?.toString(); _selectedDoctorName = name; }); Navigator.pop(ctx); }, ); }, ), ), ], ), ); }, loading: () => const SizedBox( height: 200, child: Center(child: CircularProgressIndicator()), ), error: (_, _) => const SizedBox(height: 200, child: Center(child: Text('加载失败'))), ); }, ), ); } @override Widget build(BuildContext context) { final authState = ref.watch(authProvider); if (authState.isLoggedIn && !authState.isLoading) { WidgetsBinding.instance.addPostFrameCallback((_) => _goHome()); } 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, ), ), ), ], ), ), ), ), ), ); } } class _BrandMark extends StatelessWidget { @override Widget build(BuildContext context) { return Container( width: 68, height: 68, decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(22), boxShadow: AppColors.buttonShadow, ), child: const Icon( Icons.monitor_heart_outlined, color: Colors.white, size: 34, ), ); } } class _TextField extends StatelessWidget { final TextEditingController ctrl; final String hint; final TextInputType type; final int maxLen; const _TextField(this.ctrl, this.hint, this.type, this.maxLen); @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: ''), ); } class _DoctorSelector extends StatelessWidget { final String? name; final VoidCallback onTap; const _DoctorSelector({required this.name, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Row( children: [ Expanded( child: Text( name ?? '请选择医生', style: TextStyle( fontSize: 15, color: name != null ? AppColors.textPrimary : AppColors.textHint, ), ), ), const Icon(Icons.keyboard_arrow_down, color: AppColors.textHint), ], ), ), ); } } class _SmsButton extends StatelessWidget { final bool sending; final int countdown; final VoidCallback? onTap; const _SmsButton({ required this.sending, required this.countdown, required this.onTap, }); @override Widget build(BuildContext context) { final disabled = countdown > 0 || sending; return GestureDetector( onTap: onTap, child: Container( width: 108, height: 50, alignment: Alignment.center, decoration: BoxDecoration( color: disabled ? AppColors.cardInner : AppColors.primary, borderRadius: BorderRadius.circular(12), border: Border.all( color: disabled ? AppColors.border : AppColors.primary, ), ), child: Text( sending ? '发送中' : countdown > 0 ? '${countdown}s' : '获取验证码', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: disabled ? AppColors.textSecondary : Colors.white, ), ), ), ); } } class _Agreement extends StatelessWidget { final bool agreed; final VoidCallback onTap; const _Agreement({required this.agreed, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 18, height: 18, margin: const EdgeInsets.only(right: 6), decoration: BoxDecoration( color: agreed ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(5), border: Border.all( color: agreed ? AppColors.primary : AppColors.border, ), ), child: agreed ? const Icon(Icons.check, size: 13, color: Colors.white) : null, ), Flexible( child: RichText( text: const TextSpan( children: [ TextSpan( text: '已阅读并同意', style: TextStyle(fontSize: 12, color: AppColors.textHint), ), TextSpan( text: '《服务协议》', style: TextStyle( fontSize: 12, color: AppColors.primary, fontWeight: FontWeight.w700, ), ), TextSpan( text: '和', style: TextStyle(fontSize: 12, color: AppColors.textHint), ), TextSpan( text: '《隐私政策》', style: TextStyle( fontSize: 12, color: AppColors.primary, fontWeight: FontWeight.w700, ), ), ], ), ), ), ], ), ); } } class _NoticeBanner extends StatelessWidget { final String text; final bool success; const _NoticeBanner({required this.text, required this.success}); @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: success ? AppColors.successLight : AppColors.errorLight, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( success ? Icons.check_circle : Icons.error_outline, color: success ? AppColors.success : AppColors.error, size: 18, ), const SizedBox(width: 8), Expanded( child: Text( text, style: TextStyle( color: success ? AppColors.success : AppColors.error, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ], ), ); } } class _PrimaryButton extends StatelessWidget { final bool loading; final String label; final VoidCallback? onTap; const _PrimaryButton({ required this.loading, required this.label, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(14), boxShadow: AppColors.buttonShadow, ), child: loading ? const SizedBox( width: 23, height: 23, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : Text( label, style: const TextStyle( fontSize: 17, color: Colors.white, fontWeight: FontWeight.w800, ), ), ), ); } }