| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- 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<ChangePasswordScreen> createState() =>
- _ChangePasswordScreenState();
- }
- class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
- 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<void> _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<ChangePasswordState>(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<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.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,
- ),
- ),
- ),
- ),
- );
- }
- }
|