import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../providers/change_password_provider.dart'; class ChangePasswordScreen extends ConsumerStatefulWidget { const ChangePasswordScreen({super.key}); @override ConsumerState createState() => _ChangePasswordScreenState(); } class _ChangePasswordScreenState extends ConsumerState { final _currentController = TextEditingController(); final _newController = TextEditingController(); final _confirmController = TextEditingController(); final _codeController = TextEditingController(); bool _obscureCurrent = true; bool _obscureNew = true; bool _obscureConfirm = true; // 新密码强度 bool get _hasLength => _newController.text.length >= 6 && _newController.text.length <= 16; bool get _hasLetter => _newController.text.contains(RegExp(r'[a-zA-Z]')); bool get _hasNumber => _newController.text.contains(RegExp(r'[0-9]')); bool get _newPasswordValid => _hasLength && _hasLetter && _hasNumber; bool get _canSubmit => _currentController.text.isNotEmpty && _newPasswordValid && _confirmController.text.isNotEmpty && _codeController.text.length == 6; @override void dispose() { _currentController.dispose(); _newController.dispose(); _confirmController.dispose(); _codeController.dispose(); super.dispose(); } Future _handleSubmit() async { final l10n = AppLocalizations.of(context)!; if (_newController.text != _confirmController.text) { showTopToast(context, message: l10n.passwordMismatch); return; } final success = await ref.read(changePasswordProvider.notifier).changePassword( oldPassword: _currentController.text, newPassword: _newController.text, vcode: _codeController.text, ); if (success && mounted) { showTopToast(context, message: l10n.passwordChanged, backgroundColor: AppColors.success); context.pop(); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final state = ref.watch(changePasswordProvider); final pwdTyped = _newController.text.isNotEmpty; final metCount = [_hasLength, _hasLetter, _hasNumber].where((m) => m).length; ref.listen(changePasswordProvider, (prev, next) { if (next.errorMessage != null && next.errorMessage != prev?.errorMessage) { final l10n = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(next.errorMessage!, l10n) ?? next.errorMessage!); ref.read(changePasswordProvider.notifier).clearError(); } }); return Scaffold( appBar: AppBar( elevation: 0, leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text( AppLocalizations.of(context)!.changeLoginPassword, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 当前密码 ────────────────────────────────── _FieldLabel(label: AppLocalizations.of(context)!.currentPassword), const SizedBox(height: 10), _InputField( controller: _currentController, hint: AppLocalizations.of(context)!.currentPasswordHint, obscure: _obscureCurrent, onChanged: (_) => setState(() {}), suffixIcon: _EyeButton( obscure: _obscureCurrent, onTap: () => setState(() => _obscureCurrent = !_obscureCurrent), ), ), const SizedBox(height: 20), // ── 新密码 ──────────────────────────────────── _FieldLabel(label: AppLocalizations.of(context)!.newPassword), const SizedBox(height: 10), _InputField( controller: _newController, hint: AppLocalizations.of(context)!.loginPasswordHint, obscure: _obscureNew, onChanged: (_) => setState(() {}), suffixIcon: _EyeButton( obscure: _obscureNew, onTap: () => setState(() => _obscureNew = !_obscureNew), ), ), const SizedBox(height: 10), // 密码强度 chips + 进度条 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: cs.onSurface.withAlpha(100)), ), ), child: Column( children: [ Row( children: [ _StrengthChip(label: AppLocalizations.of(context)!.passwordLengthHint, met: _hasLength, typed: pwdTyped), const SizedBox(width: 8), _StrengthChip(label: AppLocalizations.of(context)!.containsLetter, met: _hasLetter, typed: pwdTyped), const SizedBox(width: 8), _StrengthChip(label: AppLocalizations.of(context)!.containsDigit, met: _hasNumber, typed: pwdTyped), ], ), const SizedBox(height: 8), Row( children: List.generate(3, (i) { final Color barColor; if (!pwdTyped) { barColor = cs.onSurface.withAlpha(40); } else if (i < metCount) { barColor = metCount == 3 ? AppColors.rise : AppColors.fall; } else { barColor = cs.onSurface.withAlpha(40); } return Expanded( child: Container( height: 3, margin: EdgeInsets.only(right: i < 2 ? 6 : 0), decoration: BoxDecoration( color: barColor, borderRadius: BorderRadius.circular(2), ), ), ); }), ), ], ), ), const SizedBox(height: 20), // ── 确认新密码 ──────────────────────────────── _FieldLabel(label: AppLocalizations.of(context)!.confirmNewPassword), const SizedBox(height: 10), _InputField( controller: _confirmController, hint: AppLocalizations.of(context)!.confirmNewPasswordHint, obscure: _obscureConfirm, onChanged: (_) => setState(() {}), suffixIcon: _EyeButton( obscure: _obscureConfirm, onTap: () => setState(() => _obscureConfirm = !_obscureConfirm), ), ), const SizedBox(height: 20), // ── 邮箱验证码 ──────────────────────────────── _FieldLabel(label: AppLocalizations.of(context)!.emailCode), const SizedBox(height: 10), Row( children: [ Expanded( child: _InputField( controller: _codeController, hint: AppLocalizations.of(context)!.emailCodeHint, keyboardType: TextInputType.number, maxLength: 6, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (_) => setState(() {}), ), ), const SizedBox(width: 12), _SendCodeButton( cooldown: state.codeCooldown, isLoading: state.isSendingCode, onTap: state.codeCooldown == 0 && !state.isSendingCode ? () => ref.read(changePasswordProvider.notifier).sendEmailCode() : null, ), ], ), const SizedBox(height: 8), Text( AppLocalizations.of(context)!.checkSpamMessage, style: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 12), ), const SizedBox(height: 32), // ── 确认按钮 ────────────────────────────────── SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: (_canSubmit && !state.isLoading) ? _handleSubmit : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, disabledBackgroundColor: cs.outline.withAlpha(30), foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: state.isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black, ), ) : Text( AppLocalizations.of(context)!.confirmModify, style: TextStyle( color: (_canSubmit && !state.isLoading) ? Colors.black : cs.onSurface.withAlpha(153), fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ); } } // ── 密码强度 Chip ───────────────────────────────────────────── class _StrengthChip extends StatelessWidget { const _StrengthChip({required this.label, required this.met, this.typed = false}); final String label; final bool met; final bool typed; @override Widget build(BuildContext context) { final Color color; final IconData icon; if (!typed) { color = Theme.of(context).colorScheme.onSurface.withAlpha(120); icon = Icons.circle; } else if (met) { color = AppColors.rise; icon = Icons.check; } else { color = AppColors.fall; icon = Icons.close; } return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: color.withAlpha(80), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: icon == Icons.circle ? 6 : 12, color: color), const SizedBox(width: 4), Text(label, style: TextStyle(color: color, fontSize: 11)), ], ), ); } } // ── 字段标签 ───────────────────────────────────────────────── class _FieldLabel extends StatelessWidget { const _FieldLabel({required this.label}); final String label; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Text( label, style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w500, ), ); } } // ── 输入框 ──────────────────────────────────────────────────── class _InputField extends StatelessWidget { const _InputField({ required this.controller, required this.hint, this.obscure = false, this.suffixIcon, this.onChanged, this.keyboardType, this.maxLength, this.inputFormatters, }); final TextEditingController controller; final String hint; final bool obscure; final Widget? suffixIcon; final ValueChanged? onChanged; final TextInputType? keyboardType; final int? maxLength; final List? inputFormatters; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return TextField( controller: controller, obscureText: obscure, keyboardType: keyboardType, maxLength: maxLength, inputFormatters: inputFormatters, onChanged: onChanged, style: TextStyle(color: cs.onSurface, fontSize: 14), decoration: InputDecoration( counterText: '', hintText: hint, hintStyle: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), suffixIcon: suffixIcon, ), ); } } // ── 显示/隐藏密码按钮 ───────────────────────────────────────── class _EyeButton extends StatelessWidget { const _EyeButton({required this.obscure, required this.onTap}); final bool obscure; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return IconButton( onPressed: onTap, icon: Icon( obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: cs.onSurface.withAlpha(153), size: 20, ), ); } } // ── 发送验证码按钮 ──────────────────────────────────────────── class _SendCodeButton extends StatelessWidget { const _SendCodeButton({ required this.cooldown, required this.isLoading, required this.onTap, }); final int cooldown; final bool isLoading; final VoidCallback? onTap; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final enabled = onTap != null; return GestureDetector( onTap: onTap, child: Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( color: enabled ? AppColors.brand : cs.outline.withAlpha(30), borderRadius: BorderRadius.circular(10), ), child: Center( child: isLoading ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: cs.onSurface.withAlpha(153), ), ) : Text( cooldown > 0 ? '${cooldown}s' : AppLocalizations.of(context)!.sendCode, style: TextStyle( color: enabled ? Colors.black : cs.onSurface.withAlpha(153), fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ), ); } }