change_password_page.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import '../../../core/utils/common_utils.dart';
  5. import '../../../core/utils/toast_utils.dart';
  6. import '../../../core/utils/logger.dart';
  7. import '../../../l10n/app_localizations.dart';
  8. import '../../../module/common_input_field.dart';
  9. import '../../auth/domain/login_service.dart';
  10. import '../data/change_password_provider.dart';
  11. import '../domain/change_password_service.dart';
  12. class ChangePasswordPage extends ConsumerStatefulWidget {
  13. const ChangePasswordPage({super.key});
  14. @override
  15. ConsumerState<ChangePasswordPage> createState() => _ChangePasswordPageState();
  16. }
  17. class _ChangePasswordPageState extends ConsumerState<ChangePasswordPage> {
  18. Timer? _smsTimer;
  19. // 验证密码一致性(回车键/Tab键触发)
  20. void _validatePasswordMatch() {
  21. final password = ref.read(changePwdPasswordProvider);
  22. final confirmPassword = ref.read(changePwdConfirmPasswordProvider);
  23. ref.read(changePwdShowPasswordErrorProvider.notifier).state =
  24. password.isNotEmpty &&
  25. confirmPassword.isNotEmpty &&
  26. password != confirmPassword;
  27. }
  28. void startSmsCountdown() {
  29. ref.read(changePwdSmsCountdownProvider.notifier).state = 60;
  30. _smsTimer?.cancel();
  31. _smsTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  32. final value = ref.read(changePwdSmsCountdownProvider);
  33. if (value <= 1) {
  34. timer.cancel();
  35. ref.read(changePwdSmsCountdownProvider.notifier).state = 0;
  36. } else {
  37. ref.read(changePwdSmsCountdownProvider.notifier).state = value - 1;
  38. }
  39. });
  40. }
  41. void submit(BuildContext context) async {
  42. final l10n = AppLocalizations.of(context)!;
  43. final phone = ref.read(changePwdPhoneProvider);
  44. final smsCode = ref.read(changePwdServerSmsCodeProvider);
  45. final password = ref.read(changePwdPasswordProvider);
  46. final confirmPassword = ref.read(changePwdConfirmPasswordProvider);
  47. final error = validateForm(
  48. context: context,
  49. phone: phone,
  50. code: smsCode,
  51. password: password,
  52. confirmPassword: confirmPassword,
  53. );
  54. if (error != null) {
  55. ToastUtils.showError(error);
  56. return;
  57. }
  58. // 调用重置密码接口
  59. try {
  60. final response = await ChangePasswordService.changePassword(
  61. mobile: phone,
  62. oldPassword: password,
  63. newPassword: confirmPassword,
  64. smsCode: smsCode,
  65. );
  66. if (response.success && response.code == 20000) {
  67. AppLogger.d('修改密码成功');
  68. ToastUtils.showSuccess(l10n.changePasswordSuccess);
  69. if (mounted) {
  70. Navigator.of(context).pop();
  71. }
  72. } else {
  73. final errorMsg = response.msg ?? l10n.changePasswordFailed;
  74. AppLogger.e(errorMsg);
  75. ToastUtils.showError(errorMsg);
  76. }
  77. } catch (e) {
  78. AppLogger.e('修改密码错误', e);
  79. ToastUtils.showError(l10n.changePasswordFailed);
  80. }
  81. }
  82. @override
  83. void dispose() {
  84. _smsTimer?.cancel();
  85. super.dispose();
  86. }
  87. @override
  88. Widget build(BuildContext context) {
  89. final phone = ref.watch(changePwdPhoneProvider);
  90. final smsCode = ref.watch(changePwdServerSmsCodeProvider);
  91. final password = ref.watch(changePwdPasswordProvider);
  92. final confirmPassword = ref.watch(changePwdConfirmPasswordProvider);
  93. final pwdVisible = ref.watch(changePwdPasswordVisibleProvider);
  94. final confirmPwdVisible = ref.watch(changePwdConfirmPasswordVisibleProvider);
  95. final countdown = ref.watch(changePwdSmsCountdownProvider);
  96. final smsHasSent = ref.watch(changePwdSmsHasSentProvider);
  97. final showPasswordError = ref.watch(changePwdShowPasswordErrorProvider);
  98. final canSubmit = validateForm(
  99. context: context,
  100. phone: phone,
  101. code: smsCode,
  102. password: password,
  103. confirmPassword: confirmPassword) == null;
  104. final l10n = AppLocalizations.of(context)!;
  105. return Dialog(
  106. insetPadding: const EdgeInsets.all(24),
  107. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
  108. child: Padding(
  109. padding: const EdgeInsets.all(20),
  110. child: Column(
  111. mainAxisSize: MainAxisSize.min,
  112. children: [
  113. /// 标题
  114. Row(
  115. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  116. children: [
  117. Text(
  118. l10n.forgotPassword,
  119. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  120. ),
  121. IconButton(
  122. icon: const Icon(Icons.close),
  123. onPressed: () => Navigator.of(context).pop(),
  124. )
  125. ],
  126. ),
  127. const SizedBox(height: 16),
  128. /// 手机号
  129. CommonInputField(
  130. label: l10n.phoneNumber,
  131. required: true,
  132. value: phone,
  133. hintText: l10n.phoneNumberRequired,
  134. keyboardType: TextInputType.phone,
  135. onChanged: (v) =>
  136. ref.read(changePwdPhoneProvider.notifier).state = v,
  137. ),
  138. const SizedBox(height: 16),
  139. /// 验证码
  140. CommonInputField(
  141. label: l10n.smsCode,
  142. required: true,
  143. value: smsCode,
  144. hintText: l10n.smsCodeRequired,
  145. keyboardType: TextInputType.number,
  146. onChanged: (v) =>
  147. ref.read(changePwdServerSmsCodeProvider.notifier).state = v,
  148. suffix: TextButton(
  149. onPressed: countdown > 0
  150. ? null
  151. : () async {
  152. // 校验手机号
  153. if (phone.isEmpty) {
  154. ToastUtils.showError(l10n.phoneNumberRequired);
  155. return;
  156. }
  157. if (!RegExp(r'^1\d{10}$').hasMatch(phone)) {
  158. ToastUtils.showError(l10n.phoneNumberInvalid);
  159. return;
  160. }
  161. // 发送验证码
  162. try {
  163. AppLogger.d('修改密码 - 发送验证码,手机号: $phone');
  164. final smsCode = await LoginService.sendSmsCode(
  165. mobile: phone,
  166. scope: 'yun-his-forget-password-sms-send',
  167. );
  168. // 保存服务器返回的验证码
  169. ref.read(changePwdServerSmsCodeProvider.notifier).state = smsCode;
  170. AppLogger.d('验证码已保存: $smsCode');
  171. ToastUtils.showSuccess(l10n.smsCodeSent);
  172. ref.read(changePwdSmsHasSentProvider.notifier).state = true;
  173. startSmsCountdown();
  174. } catch (e) {
  175. AppLogger.e('发送验证码失败', e);
  176. ToastUtils.showError(e.toString().replaceAll('Exception: ', ''));
  177. }
  178. },
  179. child: Text(
  180. countdown > 0
  181. ? '${countdown}s'
  182. : (smsHasSent ? l10n.resendSmsCode : l10n.getSmsCode),
  183. ),
  184. ),
  185. ),
  186. const SizedBox(height: 16),
  187. /// 新密码
  188. CommonInputField(
  189. label: l10n.newPassword,
  190. required: true,
  191. value: password,
  192. hintText: l10n.newPasswordRequired,
  193. obscureText: !pwdVisible,
  194. onChanged: (v) =>
  195. ref.read(changePwdPasswordProvider.notifier).state = v,
  196. suffix: IconButton(
  197. icon: Icon(
  198. pwdVisible ? Icons.visibility : Icons.visibility_off),
  199. onPressed: () => ref
  200. .read(changePwdPasswordVisibleProvider.notifier)
  201. .state = !pwdVisible,
  202. ),
  203. ),
  204. const SizedBox(height: 6),
  205. Align(
  206. alignment: Alignment.centerLeft,
  207. child: Text(
  208. l10n.passwordFormatError,
  209. style: TextStyle(fontSize: 12, color: Colors.grey),
  210. ),
  211. ),
  212. const SizedBox(height: 16),
  213. /// 确认密码
  214. CommonInputField(
  215. label: l10n.confirmPassword,
  216. required: true,
  217. value: confirmPassword,
  218. hintText: l10n.confirmPasswordRequired,
  219. obscureText: !confirmPwdVisible,
  220. onChanged: (v) {
  221. ref.read(changePwdConfirmPasswordProvider.notifier).state = v;
  222. // 用户正在输入,清除错误提示
  223. ref.read(changePwdShowPasswordErrorProvider.notifier).state = false;
  224. },
  225. onFieldSubmitted: (v) {
  226. // 回车键或 Tab 键时验证密码一致性
  227. _validatePasswordMatch();
  228. },
  229. suffix: IconButton(
  230. icon: Icon(confirmPwdVisible
  231. ? Icons.visibility
  232. : Icons.visibility_off),
  233. onPressed: () => ref
  234. .read(changePwdConfirmPasswordVisibleProvider.notifier)
  235. .state = !confirmPwdVisible,
  236. ),
  237. ),
  238. // 显示密码不一致错误提示(仅在回车/Tab时显示)
  239. if (showPasswordError)
  240. Padding(
  241. padding: const EdgeInsets.only(top: 8.0, left: 4.0),
  242. child: Text(
  243. l10n.confirmPasswordError,
  244. style: const TextStyle(
  245. color: Colors.red,
  246. fontSize: 12,
  247. ),
  248. ),
  249. ),
  250. const SizedBox(height: 24),
  251. /// 按钮
  252. Row(
  253. mainAxisAlignment: MainAxisAlignment.end,
  254. children: [
  255. OutlinedButton(
  256. onPressed: () => Navigator.of(context).pop(),
  257. child: Text(l10n.cancel),
  258. ),
  259. const SizedBox(width: 12),
  260. ElevatedButton(
  261. onPressed: canSubmit ? () => submit(context) : null,
  262. child: Text(l10n.confirm),
  263. ),
  264. ],
  265. )
  266. ],
  267. ),
  268. ),
  269. );
  270. }
  271. }