| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- 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<FundPasswordScreen> createState() =>
- _FundPasswordScreenState();
- }
- class _FundPasswordScreenState extends ConsumerState<FundPasswordScreen> {
- 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<void> _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<FundPasswordState>(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<String>? onChanged;
- final TextInputType? keyboardType;
- final int? maxLength;
- final List<TextInputFormatter>? 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,
- ),
- ),
- ),
- ),
- );
- }
- }
|