import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/banner/app_banner.dart'; import '../../../../shared/widgets/confirm_sheet.dart'; import '../../../../shared/widgets/link_button.dart'; import '../../../../shared/widgets/phone_prefix_selector.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/auth_repository.dart'; import '../../presentation/bloc/auth_bloc.dart'; import '../../presentation/bloc/auth_event.dart'; import '../../presentation/cubits/login_cubit.dart'; import '../widgets/auth_field.dart'; import '../widgets/auth_page_scaffold.dart'; class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => LoginCubit(sl()), child: const LoginView(), ); } } class LoginView extends StatefulWidget { const LoginView({super.key}); @override State createState() => _LoginViewState(); } class _LoginViewState extends State { static const _dialCodes = ['+86', '+1', '+44', '+81', '+65']; final _phoneController = TextEditingController(); final _codeController = TextEditingController(); bool _agreedToTerms = false; @override void dispose() { _phoneController.dispose(); _codeController.dispose(); super.dispose(); } Future _handleLogin() async { final cubit = context.read(); cubit.phoneChanged(_phoneController.text); cubit.codeChanged(_codeController.text); if (!cubit.state.isValid) { return; } final response = await cubit.submit(); if (response != null && mounted) { context.read().add(AuthLoggedIn(user: response.user)); context.go(AppRoutes.homeMain); } } Future _handleSendCode() async { if (!_agreedToTerms) { final confirmed = await _showAgreementDialog(); if (!confirmed || !mounted) return; setState(() => _agreedToTerms = true); } final cubit = context.read(); cubit.phoneChanged(_phoneController.text); final sent = await cubit.sendCode(); if (!mounted || !sent) { return; } } Future _showAgreementDialog() async { return await showConfirmSheet( context, title: '请先同意协议', message: '在使用我们的服务之前,请先阅读并同意《用户协议》和《隐私政策》。\n\n只有您同意上述协议,我们才能为您提供服务。', confirmText: '确认', cancelText: '取消', ); } Widget _buildAgreementCheckbox() { return Center( child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Semantics( label: '同意用户协议与隐私政策', checked: _agreedToTerms, button: true, child: InkWell( borderRadius: BorderRadius.circular(AppRadius.md), onTap: () => setState(() => _agreedToTerms = !_agreedToTerms), child: SizedBox( width: 44, height: 44, child: Center( child: Container( width: 20, height: 20, margin: const EdgeInsets.only(right: AppSpacing.sm), decoration: BoxDecoration( color: _agreedToTerms ? AppColors.blue600 : Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( color: _agreedToTerms ? AppColors.blue600 : AppColors.slate400, width: 1.5, ), ), child: _agreedToTerms ? const Icon( Icons.check, size: 14, color: AppColors.white, ) : null, ), ), ), ), ), RichText( text: TextSpan( style: const TextStyle(fontSize: 13, color: AppColors.slate600), children: [ const TextSpan(text: '我已同意'), TextSpan( text: '《用户协议》', style: const TextStyle( color: AppColors.blue600, decoration: TextDecoration.underline, ), ), const TextSpan(text: '与'), TextSpan( text: '《隐私政策》', style: const TextStyle( color: AppColors.blue600, decoration: TextDecoration.underline, ), ), ], ), ), ], ), ); } @override Widget build(BuildContext context) { return AuthPageScaffold( resizeOnKeyboard: false, mainContentKey: const Key('login_main_content'), mainContent: LayoutBuilder( builder: (context, constraints) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; return SingleChildScrollView( padding: EdgeInsets.fromLTRB( AppSpacing.lg, 0, AppSpacing.lg, bottomInset + AppSpacing.lg, ), child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 320), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const AuthHeroHeader(showBrand: true), SizedBox(height: AppSpacing.xxl), BlocBuilder( builder: (context, state) { final fieldError = state.phone.displayError != null ? state.phone.error : state.code.displayError != null ? state.code.error : null; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AuthSection( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AuthField( hint: '输入手机号', controller: _phoneController, onChanged: (value) { context.read().phoneChanged( value, ); }, keyboardType: TextInputType.phone, prefix: PhonePrefixSelector( value: state.dialCode, items: _dialCodes, onChanged: (value) { context .read() .dialCodeChanged(value); }, ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(14), ], ), SizedBox(height: AppSpacing.lg), AuthField( hint: '输入验证码', controller: _codeController, onChanged: (value) { context.read().codeChanged( value, ); }, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6), ], suffixIcon: Padding( padding: const EdgeInsets.only( right: AppSpacing.md, ), child: LinkButton( text: state.resendCooldownSeconds > 0 ? '${state.resendCooldownSeconds}s' : '发送验证码', onTap: state.canSendCode ? _handleSendCode : null, ), ), ), if (state.errorMessage != null || fieldError != null) Padding( padding: const EdgeInsets.only( top: AppSpacing.md, ), child: AppBanner( message: state.errorMessage ?? fieldError!, type: state.errorMessage != null ? ToastType.error : ToastType.warning, title: state.errorMessage != null ? '登录失败' : '请检查输入', ), ), ], ), ), SizedBox(height: AppSpacing.xxl), AppButton( text: '登录/注册', onPressed: state.status == FormzSubmissionStatus.inProgress ? null : _handleLogin, isLoading: state.status == FormzSubmissionStatus.inProgress, ), SizedBox(height: AppSpacing.xxl * 2), _buildAgreementCheckbox(), ], ); }, ), ], ), ), ), ), ); }, ), ); } }