import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/api_client.dart'; import '../core/app_exception.dart'; import '../core/error_l10n.dart'; import '../l10n/app_localizations.dart'; /// 登录页(P3 计划 Task 3)。 /// /// 账号/密码表单 → [ApiClient.login](会话由客户端自动持久化到 /// SessionStore)→ 成功 pushReplacement /home;失败按错误码本地化提示 /// (AUTH_RATE_LIMITED 文案自带「稍后再试」;网络/校验错误各有专属文案)。 /// 提交期间按钮进入加载态防重复提交。 class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState { final _formKey = GlobalKey(); final _accountController = TextEditingController(); final _passwordController = TextEditingController(); bool _submitting = false; @override void dispose() { _accountController.dispose(); _passwordController.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() => _submitting = true); final l10n = AppLocalizations.of(context); final messenger = ScaffoldMessenger.of(context); final navigator = Navigator.of(context); try { await ref.read(apiClientProvider).login( _accountController.text.trim(), _passwordController.text, ); navigator.pushReplacementNamed('/home'); } on AppException catch (error) { // 429(AUTH_RATE_LIMITED)等文案经错误码映射;登录页的 // AUTH_INVALID_CREDENTIALS 即「账号或密码错误」。 messenger.showSnackBar( SnackBar(content: Text(l10n.messageForError(error))), ); } on Object { messenger.showSnackBar( SnackBar(content: Text(l10n.errorGeneric)), ); } finally { if (mounted) setState(() => _submitting = false); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar(title: Text(l10n.appName)), body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Padding( padding: const EdgeInsets.all(24), child: Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _accountController, autofillHints: const [AutofillHints.username], decoration: InputDecoration( labelText: l10n.username, border: const OutlineInputBorder(), ), validator: (value) => (value == null || value.trim().isEmpty) ? l10n.fieldRequired : null, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, obscureText: true, enableSuggestions: false, autocorrect: false, autofillHints: const [AutofillHints.password], onFieldSubmitted: (_) => _submitting ? null : _submit(), decoration: InputDecoration( labelText: l10n.password, border: const OutlineInputBorder(), ), validator: (value) => (value == null || value.isEmpty) ? l10n.fieldRequired : null, ), const SizedBox(height: 24), FilledButton( onPressed: _submitting ? null : _submit, child: _submitting ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(l10n.login), ), const SizedBox(height: 12), Text( l10n.loginHint, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ), ), ); } }