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/fund_password_provider.dart'; /// 资金密码页面 /// [isResetMode] true=修改密码(需验证码),false=首次设置(无需验证码) class FundPasswordScreen extends ConsumerStatefulWidget { const FundPasswordScreen({super.key, this.isResetMode = false}); final bool isResetMode; @override ConsumerState createState() => _FundPasswordScreenState(); } class _FundPasswordScreenState extends ConsumerState { final _passwordController = TextEditingController(); final _confirmController = TextEditingController(); final _codeController = TextEditingController(); bool _obscurePassword = true; bool _obscureConfirm = true; @override void initState() { super.initState(); Future.microtask(() => ref.invalidate(fundPasswordProvider)); } @override void dispose() { _passwordController.dispose(); _confirmController.dispose(); _codeController.dispose(); super.dispose(); } bool get _canSubmit { final pwdFilled = _passwordController.text.isNotEmpty && _confirmController.text.isNotEmpty; if (widget.isResetMode) { return pwdFilled && _codeController.text.length == 6; } return pwdFilled; } Future _handleSubmit() async { final password = _passwordController.text; final confirm = _confirmController.text; final l10n = AppLocalizations.of(context)!; // 校验两次密码一致 if (password != confirm) { showTopToast(context, message: l10n.passwordMismatch); return; } // 校验密码格式 if (!FundPasswordNotifier.validatePassword(password)) { showTopToast(context, message: l10n.fundPasswordFormatError); return; } final notifier = ref.read(fundPasswordProvider.notifier); bool success; if (widget.isResetMode) { success = await notifier.resetPassword( newPassword: password, vcode: _codeController.text, ); } else { success = await notifier.setPassword(password); } if (success && mounted) { context.pop(); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final state = ref.watch(fundPasswordProvider); final isReset = widget.isResetMode; // 监听错误和成功 ref.listen(fundPasswordProvider, (prev, next) { if (next.errorMessage != null && next.errorMessage != prev?.errorMessage) { final l10nE = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(next.errorMessage!, l10nE) ?? next.errorMessage!); ref.read(fundPasswordProvider.notifier).clearError(); } if (next.successMessage != null && next.successMessage != prev?.successMessage) { final l10nS = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(next.successMessage!, l10nS) ?? next.successMessage!, backgroundColor: AppColors.success); ref.read(fundPasswordProvider.notifier).clearSuccess(); } }); return Scaffold( appBar: AppBar( elevation: 0, leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text( isReset ? AppLocalizations.of(context)!.changeFundPassword : AppLocalizations.of(context)!.setFundPassword, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── 提示横幅 ────────────────────────────────── _InfoBanner( text: AppLocalizations.of(context)!.fundPasswordBannerTip, ), const SizedBox(height: 24), // ── 资金密码 ────────────────────────────────── _FieldLabel(label: AppLocalizations.of(context)!.fundPassword), const SizedBox(height: 4), Text( AppLocalizations.of(context)!.fundPasswordHint, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 12), ), const SizedBox(height: 10), _InputField( controller: _passwordController, hint: AppLocalizations.of(context)!.setFundPasswordHint, obscure: _obscurePassword, onChanged: (_) => setState(() {}), suffixIcon: _EyeButton( obscure: _obscurePassword, onTap: () => setState(() => _obscurePassword = !_obscurePassword), ), ), const SizedBox(height: 20), // ── 确认资金密码 ────────────────────────────── _FieldLabel(label: AppLocalizations.of(context)!.confirmFundPassword), const SizedBox(height: 10), _InputField( controller: _confirmController, hint: AppLocalizations.of(context)!.confirmFundPasswordHint, obscure: _obscureConfirm, onChanged: (_) => setState(() {}), suffixIcon: _EyeButton( obscure: _obscureConfirm, onTap: () => setState(() => _obscureConfirm = !_obscureConfirm), ), ), // ── 邮箱验证码(仅修改模式)────────────────── if (isReset) ...[ 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: 10), _SendCodeButton( countdown: state.codeCooldown, isLoading: state.isSendingCode, onTap: state.codeCooldown == 0 && !state.isSendingCode ? () { ref .read(fundPasswordProvider.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 ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black, ), ) : Text( isReset ? AppLocalizations.of(context)!.confirmModify : AppLocalizations.of(context)!.confirmSet, style: TextStyle( color: _canSubmit ? Colors.black : cs.onSurface.withAlpha(153), fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ); } } // ── 共享组件 ───────────────────────────────────────────────── class _InfoBanner extends StatelessWidget { const _InfoBanner({required this.text}); final String text; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: const Color(0xFF2D2500), border: Border.all(color: const Color(0xFF4A3A00)), borderRadius: BorderRadius.circular(10), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.info_outline, color: AppColors.brand, size: 16), const SizedBox(width: 8), Expanded( child: Text( text, style: const TextStyle( color: AppColors.brand, fontSize: 13, height: 1.4), ), ), ], ), ); } } 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.countdown, required this.isLoading, required this.onTap, }); final int countdown; 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( countdown > 0 ? '${countdown}s' : AppLocalizations.of(context)!.sendCode, style: TextStyle( color: enabled ? Colors.black : cs.onSurface.withAlpha(153), fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ), ); } }