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_codes.dart'; import '../core/error_l10n.dart'; import '../l10n/app_localizations.dart'; /// 修改密码页(P3 计划 Task 3;入口在设置页)。 /// /// 当前/新/确认三字段;客户端校验(新密码 ≥8 位、两次一致)先行, /// 服务端 [ApiClient.changePassword] 成功后 204——当前会话被服务端保留, /// 无需重登录:toast 提示后直接 pop 回设置页。 /// AUTH_INVALID_CREDENTIALS 在本页语义为「当前密码错误」(覆写通用映射)。 class PasswordChangePage extends ConsumerStatefulWidget { const PasswordChangePage({super.key}); @override ConsumerState createState() => _PasswordChangePageState(); } class _PasswordChangePageState extends ConsumerState { final _formKey = GlobalKey(); final _oldController = TextEditingController(); final _newController = TextEditingController(); final _confirmController = TextEditingController(); bool _obscureAll = true; bool _submitting = false; @override void dispose() { _oldController.dispose(); _newController.dispose(); _confirmController.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).changePassword( _oldController.text, _newController.text, ); // 成功路径:先弹提示再 pop;pop 后 SnackBar 仍挂在根 messenger 上可见。 messenger.showSnackBar( SnackBar(content: Text(l10n.passwordChangeSuccess)), ); navigator.pop(); } on AppException catch (error) { messenger.showSnackBar( SnackBar( content: Text( error.code == ErrorCodes.authInvalidCredentials ? l10n.errorOldPasswordIncorrect : 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.passwordChangeTitle)), 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: _oldController, obscureText: _obscureAll, enableSuggestions: false, autocorrect: false, decoration: InputDecoration( labelText: l10n.currentPassword, border: const OutlineInputBorder(), ), validator: (value) => (value == null || value.isEmpty) ? l10n.fieldRequired : null, ), const SizedBox(height: 16), TextFormField( controller: _newController, obscureText: _obscureAll, enableSuggestions: false, autocorrect: false, decoration: InputDecoration( labelText: l10n.newPassword, border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return l10n.fieldRequired; } if (value.length < 8) return l10n.passwordMinLength; return null; }, ), const SizedBox(height: 16), TextFormField( controller: _confirmController, obscureText: _obscureAll, enableSuggestions: false, autocorrect: false, onFieldSubmitted: (_) => _submitting ? null : _submit(), decoration: InputDecoration( labelText: l10n.confirmNewPassword, border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return l10n.fieldRequired; } if (value != _newController.text) { return l10n.passwordMismatch; } return null; }, ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: IconButton( onPressed: () => setState(() => _obscureAll = !_obscureAll), icon: Icon( _obscureAll ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), ), ), const SizedBox(height: 16), FilledButton( onPressed: _submitting ? null : _submit, child: _submitting ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(l10n.passwordChangeTitle), ), ], ), ), ), ), ), ); } }