|
@@ -1,6 +1,14 @@
|
|
|
|
|
+import 'dart:async';
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
+import '../../../core/utils/common_utils.dart';
|
|
|
|
|
+import '../../../core/utils/toast_utils.dart';
|
|
|
|
|
+import '../../../core/utils/logger.dart';
|
|
|
import '../../../l10n/app_localizations.dart';
|
|
import '../../../l10n/app_localizations.dart';
|
|
|
|
|
+import '../../../module/common_input_field.dart';
|
|
|
|
|
+import '../../auth/domain/login_service.dart';
|
|
|
|
|
+import '../data/change_password_provider.dart';
|
|
|
|
|
+import '../domain/change_password_service.dart';
|
|
|
|
|
|
|
|
class ChangePasswordPage extends ConsumerStatefulWidget {
|
|
class ChangePasswordPage extends ConsumerStatefulWidget {
|
|
|
const ChangePasswordPage({super.key});
|
|
const ChangePasswordPage({super.key});
|
|
@@ -9,21 +17,277 @@ class ChangePasswordPage extends ConsumerStatefulWidget {
|
|
|
ConsumerState<ChangePasswordPage> createState() => _ChangePasswordPageState();
|
|
ConsumerState<ChangePasswordPage> createState() => _ChangePasswordPageState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-class _ChangePasswordPageState extends ConsumerState<ChangePasswordPage>
|
|
|
|
|
- with SingleTickerProviderStateMixin {
|
|
|
|
|
|
|
+class _ChangePasswordPageState extends ConsumerState<ChangePasswordPage> {
|
|
|
|
|
+ Timer? _smsTimer;
|
|
|
|
|
+
|
|
|
|
|
+ // 验证密码一致性(回车键/Tab键触发)
|
|
|
|
|
+ void _validatePasswordMatch() {
|
|
|
|
|
+ final password = ref.read(changePwdPasswordProvider);
|
|
|
|
|
+ final confirmPassword = ref.read(changePwdConfirmPasswordProvider);
|
|
|
|
|
+ ref.read(changePwdShowPasswordErrorProvider.notifier).state =
|
|
|
|
|
+ password.isNotEmpty &&
|
|
|
|
|
+ confirmPassword.isNotEmpty &&
|
|
|
|
|
+ password != confirmPassword;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void startSmsCountdown() {
|
|
|
|
|
+ ref.read(changePwdSmsCountdownProvider.notifier).state = 60;
|
|
|
|
|
+
|
|
|
|
|
+ _smsTimer?.cancel();
|
|
|
|
|
+ _smsTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
|
|
|
+ final value = ref.read(changePwdSmsCountdownProvider);
|
|
|
|
|
+ if (value <= 1) {
|
|
|
|
|
+ timer.cancel();
|
|
|
|
|
+ ref.read(changePwdSmsCountdownProvider.notifier).state = 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ref.read(changePwdSmsCountdownProvider.notifier).state = value - 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void submit(BuildContext context) async {
|
|
|
|
|
+ final l10n = AppLocalizations.of(context)!;
|
|
|
|
|
+
|
|
|
|
|
+ final phone = ref.read(changePwdPhoneProvider);
|
|
|
|
|
+ final smsCode = ref.read(changePwdServerSmsCodeProvider);
|
|
|
|
|
+ final password = ref.read(changePwdPasswordProvider);
|
|
|
|
|
+ final confirmPassword = ref.read(changePwdConfirmPasswordProvider);
|
|
|
|
|
+
|
|
|
|
|
+ final error = validateForm(
|
|
|
|
|
+ context: context,
|
|
|
|
|
+ phone: phone,
|
|
|
|
|
+ code: smsCode,
|
|
|
|
|
+ password: password,
|
|
|
|
|
+ confirmPassword: confirmPassword,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (error != null) {
|
|
|
|
|
+ ToastUtils.showError(error);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 调用重置密码接口
|
|
|
|
|
+ try {
|
|
|
|
|
+ final response = await ChangePasswordService.changePassword(
|
|
|
|
|
+ mobile: phone,
|
|
|
|
|
+ oldPassword: password,
|
|
|
|
|
+ newPassword: confirmPassword,
|
|
|
|
|
+ smsCode: smsCode,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (response.success && response.code == 20000) {
|
|
|
|
|
+ AppLogger.d('修改密码成功');
|
|
|
|
|
+ ToastUtils.showSuccess(l10n.changePasswordSuccess);
|
|
|
|
|
+ if (mounted) {
|
|
|
|
|
+ Navigator.of(context).pop();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ final errorMsg = response.msg ?? l10n.changePasswordFailed;
|
|
|
|
|
+ AppLogger.e(errorMsg);
|
|
|
|
|
+ ToastUtils.showError(errorMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ AppLogger.e('修改密码错误', e);
|
|
|
|
|
+ ToastUtils.showError(l10n.changePasswordFailed);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ void dispose() {
|
|
|
|
|
+ _smsTimer?.cancel();
|
|
|
|
|
+ super.dispose();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@override
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
|
|
|
+ final phone = ref.watch(changePwdPhoneProvider);
|
|
|
|
|
+ final smsCode = ref.watch(changePwdServerSmsCodeProvider);
|
|
|
|
|
+ final password = ref.watch(changePwdPasswordProvider);
|
|
|
|
|
+ final confirmPassword = ref.watch(changePwdConfirmPasswordProvider);
|
|
|
|
|
+
|
|
|
|
|
+ final pwdVisible = ref.watch(changePwdPasswordVisibleProvider);
|
|
|
|
|
+ final confirmPwdVisible = ref.watch(changePwdConfirmPasswordVisibleProvider);
|
|
|
|
|
+ final countdown = ref.watch(changePwdSmsCountdownProvider);
|
|
|
|
|
+ final smsHasSent = ref.watch(changePwdSmsHasSentProvider);
|
|
|
|
|
+ final showPasswordError = ref.watch(changePwdShowPasswordErrorProvider);
|
|
|
|
|
+
|
|
|
|
|
+ final canSubmit = validateForm(
|
|
|
|
|
+ context: context,
|
|
|
|
|
+ phone: phone,
|
|
|
|
|
+ code: smsCode,
|
|
|
|
|
+ password: password,
|
|
|
|
|
+ confirmPassword: confirmPassword) == null;
|
|
|
|
|
+
|
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
- return Scaffold(
|
|
|
|
|
- appBar: AppBar(
|
|
|
|
|
- title: Text(l10n.changePassword),
|
|
|
|
|
- elevation: 0,
|
|
|
|
|
|
|
+ return Dialog(
|
|
|
|
|
+ insetPadding: const EdgeInsets.all(24),
|
|
|
|
|
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
|
|
|
+ child: Padding(
|
|
|
|
|
+ padding: const EdgeInsets.all(20),
|
|
|
|
|
+ child: Column(
|
|
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ /// 标题
|
|
|
|
|
+ Row(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ Text(
|
|
|
|
|
+ l10n.forgotPassword,
|
|
|
|
|
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
|
|
+ ),
|
|
|
|
|
+ IconButton(
|
|
|
|
|
+ icon: const Icon(Icons.close),
|
|
|
|
|
+ onPressed: () => Navigator.of(context).pop(),
|
|
|
|
|
+ )
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+
|
|
|
|
|
+ /// 手机号
|
|
|
|
|
+ CommonInputField(
|
|
|
|
|
+ label: l10n.phoneNumber,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ value: phone,
|
|
|
|
|
+ hintText: l10n.phoneNumberRequired,
|
|
|
|
|
+ keyboardType: TextInputType.phone,
|
|
|
|
|
+ onChanged: (v) =>
|
|
|
|
|
+ ref.read(changePwdPhoneProvider.notifier).state = v,
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+
|
|
|
|
|
+ /// 验证码
|
|
|
|
|
+ CommonInputField(
|
|
|
|
|
+ label: l10n.smsCode,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ value: smsCode,
|
|
|
|
|
+ hintText: l10n.smsCodeRequired,
|
|
|
|
|
+ keyboardType: TextInputType.number,
|
|
|
|
|
+ onChanged: (v) =>
|
|
|
|
|
+ ref.read(changePwdServerSmsCodeProvider.notifier).state = v,
|
|
|
|
|
+ suffix: TextButton(
|
|
|
|
|
+ onPressed: countdown > 0
|
|
|
|
|
+ ? null
|
|
|
|
|
+ : () async {
|
|
|
|
|
+ // 校验手机号
|
|
|
|
|
+ if (phone.isEmpty) {
|
|
|
|
|
+ ToastUtils.showError(l10n.phoneNumberRequired);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
|
|
|
|
|
+ ToastUtils.showError(l10n.phoneNumberInvalid);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 发送验证码
|
|
|
|
|
+ try {
|
|
|
|
|
+ AppLogger.d('修改密码 - 发送验证码,手机号: $phone');
|
|
|
|
|
+ final smsCode = await LoginService.sendSmsCode(
|
|
|
|
|
+ mobile: phone,
|
|
|
|
|
+ scope: 'yun-his-forget-password-sms-send',
|
|
|
|
|
+ );
|
|
|
|
|
+ // 保存服务器返回的验证码
|
|
|
|
|
+ ref.read(changePwdServerSmsCodeProvider.notifier).state = smsCode;
|
|
|
|
|
+ AppLogger.d('验证码已保存: $smsCode');
|
|
|
|
|
+ ToastUtils.showSuccess(l10n.smsCodeSent);
|
|
|
|
|
+ ref.read(changePwdSmsHasSentProvider.notifier).state = true;
|
|
|
|
|
+ startSmsCountdown();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ AppLogger.e('发送验证码失败', e);
|
|
|
|
|
+ ToastUtils.showError(e.toString().replaceAll('Exception: ', ''));
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ countdown > 0
|
|
|
|
|
+ ? '${countdown}s'
|
|
|
|
|
+ : (smsHasSent ? l10n.resendSmsCode : l10n.getSmsCode),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+
|
|
|
|
|
+ /// 新密码
|
|
|
|
|
+ CommonInputField(
|
|
|
|
|
+ label: l10n.newPassword,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ value: password,
|
|
|
|
|
+ hintText: l10n.newPasswordRequired,
|
|
|
|
|
+ obscureText: !pwdVisible,
|
|
|
|
|
+ onChanged: (v) =>
|
|
|
|
|
+ ref.read(changePwdPasswordProvider.notifier).state = v,
|
|
|
|
|
+ suffix: IconButton(
|
|
|
|
|
+ icon: Icon(
|
|
|
|
|
+ pwdVisible ? Icons.visibility : Icons.visibility_off),
|
|
|
|
|
+ onPressed: () => ref
|
|
|
|
|
+ .read(changePwdPasswordVisibleProvider.notifier)
|
|
|
|
|
+ .state = !pwdVisible,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 6),
|
|
|
|
|
+ Align(
|
|
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ l10n.passwordFormatError,
|
|
|
|
|
+ style: TextStyle(fontSize: 12, color: Colors.grey),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 16),
|
|
|
|
|
+
|
|
|
|
|
+ /// 确认密码
|
|
|
|
|
+ CommonInputField(
|
|
|
|
|
+ label: l10n.confirmPassword,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ value: confirmPassword,
|
|
|
|
|
+ hintText: l10n.confirmPasswordRequired,
|
|
|
|
|
+ obscureText: !confirmPwdVisible,
|
|
|
|
|
+ onChanged: (v) {
|
|
|
|
|
+ ref.read(changePwdConfirmPasswordProvider.notifier).state = v;
|
|
|
|
|
+ // 用户正在输入,清除错误提示
|
|
|
|
|
+ ref.read(changePwdShowPasswordErrorProvider.notifier).state = false;
|
|
|
|
|
+ },
|
|
|
|
|
+ onFieldSubmitted: (v) {
|
|
|
|
|
+ // 回车键或 Tab 键时验证密码一致性
|
|
|
|
|
+ _validatePasswordMatch();
|
|
|
|
|
+ },
|
|
|
|
|
+ suffix: IconButton(
|
|
|
|
|
+ icon: Icon(confirmPwdVisible
|
|
|
|
|
+ ? Icons.visibility
|
|
|
|
|
+ : Icons.visibility_off),
|
|
|
|
|
+ onPressed: () => ref
|
|
|
|
|
+ .read(changePwdConfirmPasswordVisibleProvider.notifier)
|
|
|
|
|
+ .state = !confirmPwdVisible,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ // 显示密码不一致错误提示(仅在回车/Tab时显示)
|
|
|
|
|
+ if (showPasswordError)
|
|
|
|
|
+ Padding(
|
|
|
|
|
+ padding: const EdgeInsets.only(top: 8.0, left: 4.0),
|
|
|
|
|
+ child: Text(
|
|
|
|
|
+ l10n.confirmPasswordError,
|
|
|
|
|
+ style: const TextStyle(
|
|
|
|
|
+ color: Colors.red,
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(height: 24),
|
|
|
|
|
+
|
|
|
|
|
+ /// 按钮
|
|
|
|
|
+ Row(
|
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
|
+ children: [
|
|
|
|
|
+ OutlinedButton(
|
|
|
|
|
+ onPressed: () => Navigator.of(context).pop(),
|
|
|
|
|
+ child: Text(l10n.cancel),
|
|
|
|
|
+ ),
|
|
|
|
|
+ const SizedBox(width: 12),
|
|
|
|
|
+ ElevatedButton(
|
|
|
|
|
+ onPressed: canSubmit ? () => submit(context) : null,
|
|
|
|
|
+ child: Text(l10n.confirm),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ )
|
|
|
|
|
+ ],
|
|
|
),
|
|
),
|
|
|
- body: ListView.builder(
|
|
|
|
|
- itemCount: 5,
|
|
|
|
|
- itemBuilder: (BuildContext context, int index) {
|
|
|
|
|
- return const SizedBox.shrink();
|
|
|
|
|
- }),
|
|
|
|
|
|
|
+ ),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|