login_page.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import 'package:sino_med_cloud/l10n/app_localizations.dart';
  6. import 'package:sino_med_cloud/core/constants/app_constants.dart';
  7. import 'package:sino_med_cloud/core/utils/toast_utils.dart'; // 添加 ToastUtils 引用
  8. import '../../../core/utils/logger.dart';
  9. import '../../../core/utils/crypto_utils.dart';
  10. import '../../../core/storage/local_storage.dart';
  11. import '../../MinePage/data/change_password_provider.dart';
  12. import '../../MinePage/presentation/change_password_page.dart';
  13. import '../domain/login_service.dart';
  14. import '../data/login_provider.dart';
  15. class LoginPage extends ConsumerStatefulWidget {
  16. const LoginPage({super.key});
  17. @override
  18. ConsumerState<LoginPage> createState() => _LoginPageState();
  19. }
  20. class _LoginPageState extends ConsumerState<LoginPage>
  21. with SingleTickerProviderStateMixin {
  22. late TabController _tabController;
  23. final _passwordFormKey = GlobalKey<FormState>();
  24. final _smsFormKey = GlobalKey<FormState>();
  25. bool _listenersSetup = false;
  26. //用于test
  27. final _loginSystem = "YUN_HIS_PC_WEB";
  28. final _loginType = "MOBILE_PASSWORD";
  29. final _loginSmsType = "MOBILE_SMS_CODE";
  30. // 密码登录表单
  31. final _phoneController = TextEditingController();
  32. final _passwordController = TextEditingController();
  33. // 验证码登录表单
  34. final _phoneSmsController = TextEditingController();
  35. final _smsCodeController = TextEditingController();
  36. Timer? _countdownTimer;
  37. // 防止循环更新的标志
  38. bool _isSyncingPhone = false;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _tabController = TabController(length: 2, vsync: this);
  43. _loadSavedLoginInfo();
  44. }
  45. /// 加载保存的登录信息
  46. void _loadSavedLoginInfo() async {
  47. try {
  48. // 加载保存的手机号
  49. final savedPhone = await LocalStorage.getPhone();
  50. if (savedPhone != null && savedPhone.isNotEmpty) {
  51. _phoneController.text = savedPhone;
  52. _phoneSmsController.text = savedPhone;
  53. // 同步到 Provider,避免监听器尚未注册导致 Provider 仍为空
  54. if (mounted) {
  55. ref.read(passwordLoginPhoneProvider.notifier).state = savedPhone;
  56. ref.read(smsLoginPhoneProvider.notifier).state = savedPhone;
  57. }
  58. AppLogger.d('加载保存的手机号: $savedPhone');
  59. }
  60. // 检查是否记住密码
  61. final rememberPassword = await LocalStorage.getRememberPassword();
  62. if (mounted) {
  63. ref.read(rememberPasswordProvider.notifier).state = rememberPassword;
  64. // 回到登录页时,强制隐藏密码(无论前一次是否显示)
  65. ref.read(passwordObscureProvider.notifier).state = true;
  66. }
  67. if (rememberPassword) {
  68. // 加载保存的密码(原始密码)
  69. final savedPassword = await LocalStorage.getPassword();
  70. if (savedPassword != null && savedPassword.isNotEmpty) {
  71. // 设置密码(但保持隐藏状态)
  72. _passwordController.text = savedPassword;
  73. // 同步到 Provider,避免监听器尚未注册导致 Provider 仍为空
  74. if (mounted) {
  75. ref.read(passwordLoginPasswordProvider.notifier).state = savedPassword;
  76. }
  77. AppLogger.d('加载保存的密码(原始密码)');
  78. }
  79. }
  80. } catch (e) {
  81. AppLogger.e('加载保存的登录信息失败', e);
  82. }
  83. }
  84. void _setupListeners() {
  85. // 先强制同步一次当前 Controller 的值到 Provider(避免 _loadSavedLoginInfo 在 listener 注册前完成)
  86. ref.read(passwordLoginPhoneProvider.notifier).state = _phoneController.text;
  87. ref.read(passwordLoginPasswordProvider.notifier).state = _passwordController.text;
  88. ref.read(smsLoginPhoneProvider.notifier).state = _phoneSmsController.text;
  89. ref.read(smsLoginCodeProvider.notifier).state = _smsCodeController.text;
  90. // 监听 Tab 切换,同步到 Provider
  91. _tabController.addListener(() {
  92. if (!_tabController.indexIsChanging) {
  93. ref.read(loginTabIndexProvider.notifier).state = _tabController.index;
  94. }
  95. });
  96. // 同步初始 tab 索引
  97. ref.read(loginTabIndexProvider.notifier).state = _tabController.index;
  98. // 监听输入框变化,同步到 Provider 和另一个输入框
  99. _phoneController.addListener(() {
  100. final text = _phoneController.text;
  101. ref.read(passwordLoginPhoneProvider.notifier).state = text;
  102. // 同步到验证码登录的手机号输入框
  103. if (!_isSyncingPhone && _phoneSmsController.text != text) {
  104. _isSyncingPhone = true;
  105. _phoneSmsController.text = text;
  106. ref.read(smsLoginPhoneProvider.notifier).state = text;
  107. _isSyncingPhone = false;
  108. }
  109. });
  110. _passwordController.addListener(() {
  111. ref.read(passwordLoginPasswordProvider.notifier).state = _passwordController.text;
  112. });
  113. _phoneSmsController.addListener(() {
  114. final text = _phoneSmsController.text;
  115. ref.read(smsLoginPhoneProvider.notifier).state = text;
  116. // 同步到密码登录的手机号输入框
  117. if (!_isSyncingPhone && _phoneController.text != text) {
  118. _isSyncingPhone = true;
  119. _phoneController.text = text;
  120. ref.read(passwordLoginPhoneProvider.notifier).state = text;
  121. _isSyncingPhone = false;
  122. }
  123. });
  124. _smsCodeController.addListener(() {
  125. ref.read(smsLoginCodeProvider.notifier).state = _smsCodeController.text;
  126. });
  127. }
  128. @override
  129. void dispose() {
  130. _tabController.dispose();
  131. _phoneController.dispose();
  132. _passwordController.dispose();
  133. _phoneSmsController.dispose();
  134. _smsCodeController.dispose();
  135. _clearCountdownTimer();
  136. super.dispose();
  137. }
  138. /// 清理倒计时定时器
  139. void _clearCountdownTimer() {
  140. _countdownTimer?.cancel();
  141. _countdownTimer = null;
  142. }
  143. // 发送验证码
  144. void _sendSmsCode() async {
  145. final l10n = AppLocalizations.of(context)!;
  146. final phone = ref.read(smsLoginPhoneProvider);
  147. if (phone.isEmpty) {
  148. ToastUtils.show(l10n.phoneNumberRequired);
  149. return;
  150. }
  151. try {
  152. // 清理之前的定时器
  153. _clearCountdownTimer();
  154. // 调用发送验证码服务
  155. final smsCode = await LoginService.sendSmsCode(mobile: phone);
  156. // 保存验证码到 Provider
  157. ref.read(smsCodeFromServerProvider.notifier).state = smsCode;
  158. // 标记已获取过验证码
  159. ref.read(smsHasReceivedProvider.notifier).state = true;
  160. // 开始倒计时
  161. ref.read(smsCountdownProvider.notifier).state = AppConstants.smsCodeCountdown;
  162. // 使用 Timer 进行倒计时
  163. _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  164. if (!mounted) {
  165. timer.cancel();
  166. return;
  167. }
  168. final currentCountdown = ref.read(smsCountdownProvider);
  169. if (currentCountdown > 1) {
  170. ref.read(smsCountdownProvider.notifier).state = currentCountdown - 1;
  171. } else {
  172. // 倒计时结束(currentCountdown == 1 或 0)
  173. // 先清空验证码,再设置倒计时为0,确保 Consumer 能正确监听到变化
  174. ref.read(smsCodeFromServerProvider.notifier).state = '';
  175. ref.read(smsCountdownProvider.notifier).state = 0;
  176. timer.cancel();
  177. _countdownTimer = null;
  178. AppLogger.d('倒计时结束,验证码已清空,hasReceived: ${ref.read(smsHasReceivedProvider)}');
  179. }
  180. });
  181. } catch (e) {
  182. AppLogger.e('发送验证码错误', e);
  183. if (mounted) {
  184. ToastUtils.showError(e.toString());
  185. }
  186. }
  187. }
  188. // 密码登录
  189. void _handlePasswordLogin() async {
  190. try {
  191. // 当前手机号及密码的格式已经验证过
  192. if (_passwordFormKey.currentState!.validate()) {
  193. // 在异步方法中应使用 ref.read
  194. final phoneNumber = ref.read(passwordLoginPhoneProvider);
  195. final password = ref.read(passwordLoginPasswordProvider);
  196. final rememberPassword = ref.read(rememberPasswordProvider);
  197. // 每次登录请求时重新加密(不存 MD5)
  198. final encryptedPassword = CryptoUtils.md5(password);
  199. AppLogger.d('开始密码登录请求: $phoneNumber');
  200. // 调用登录服务
  201. await LoginService.passwordLogin(
  202. mobile: phoneNumber,
  203. password: encryptedPassword,
  204. loginSystem: _loginSystem,
  205. loginType: _loginType,
  206. );
  207. AppLogger.d('密码登录成功,准备跳转主页');
  208. // 登录成功后,保存手机号(总是保存)
  209. await LocalStorage.savePhone(phoneNumber);
  210. // 根据"记住密码"选项保存或删除密码
  211. if (rememberPassword) {
  212. // 保存原始密码(登录时再加密)
  213. await LocalStorage.savePassword(password);
  214. await LocalStorage.saveRememberPassword(true);
  215. AppLogger.d('已保存手机号和密码');
  216. } else {
  217. // 不记住密码时,删除已保存的密码
  218. await LocalStorage.removePassword();
  219. await LocalStorage.saveRememberPassword(false);
  220. AppLogger.d('已保存手机号,删除密码');
  221. }
  222. // 跳转到主页(使用 go 替换当前路由,销毁登录页)
  223. if (mounted) {
  224. context.go('/mainTab');
  225. }
  226. }
  227. } catch (e, stack) {
  228. AppLogger.e('密码登录错误', e, stack);
  229. if (mounted) {
  230. ToastUtils.showError(e.toString().replaceAll('Exception: ', ''));
  231. }
  232. }
  233. }
  234. // 验证码登录
  235. void _handleSmsLogin() async {
  236. try {
  237. final l10n = AppLocalizations.of(context)!;
  238. if (_smsFormKey.currentState!.validate()) {
  239. // 在异步方法中应使用 ref.read
  240. final smsCode = ref.read(smsLoginCodeProvider);
  241. final serverSmsCode = ref.read(smsCodeFromServerProvider);
  242. // 检查验证码是否已过期(为空或倒计时已结束)
  243. if (serverSmsCode.isEmpty) {
  244. ToastUtils.show(l10n.smsCodeHasExpired);
  245. return;
  246. }
  247. // 验证码输入错误
  248. if (smsCode != serverSmsCode) {
  249. ToastUtils.showError(l10n.smsCodeError);
  250. return;
  251. }
  252. final phoneNumber = ref.read(smsLoginPhoneProvider);
  253. AppLogger.d('开始验证码登录请求: $phoneNumber');
  254. // 调用登录服务
  255. await LoginService.smsLogin(
  256. mobile: phoneNumber,
  257. smsCode: smsCode,
  258. loginSystem: _loginSystem,
  259. loginType: _loginSmsType,
  260. );
  261. AppLogger.d('验证码登录成功,清理状态并跳转主页');
  262. // 登录成功,清理倒计时和定时器
  263. _clearCountdownTimer();
  264. ref.read(smsCountdownProvider.notifier).state = 0;
  265. ref.read(smsHasReceivedProvider.notifier).state = false;
  266. ref.read(smsCodeFromServerProvider.notifier).state = '';
  267. // 跳转到主页(使用 go 替换当前路由,销毁登录页)
  268. if (mounted) {
  269. context.go('/mainTab');
  270. }
  271. }
  272. } catch (e, stack) {
  273. AppLogger.e('验证码登录错误', e, stack);
  274. if (mounted) {
  275. ToastUtils.showError(e.toString().replaceAll('Exception: ', ''));
  276. }
  277. }
  278. }
  279. @override
  280. Widget build(BuildContext context) {
  281. // 设置监听器(只设置一次)
  282. if (!_listenersSetup) {
  283. WidgetsBinding.instance.addPostFrameCallback((_) {
  284. _setupListeners();
  285. _listenersSetup = true;
  286. });
  287. }
  288. final l10n = AppLocalizations.of(context)!;
  289. return Scaffold(
  290. body: GestureDetector(
  291. onTap: () {
  292. // 点击空白区域时收起键盘并移除焦点
  293. FocusScope.of(context).unfocus();
  294. },
  295. behavior: HitTestBehavior.opaque,
  296. child: SafeArea(
  297. child: SingleChildScrollView(
  298. padding: const EdgeInsets.all(24),
  299. child: Column(
  300. crossAxisAlignment: CrossAxisAlignment.stretch,
  301. children: [
  302. const SizedBox(height: 40),
  303. // Logo 或标题
  304. Text(
  305. l10n.appName,
  306. style: const TextStyle(
  307. fontSize: 32,
  308. fontWeight: FontWeight.bold,
  309. color: Color(0xFF1F2937),
  310. ),
  311. textAlign: TextAlign.center,
  312. ),
  313. const SizedBox(height: 8),
  314. Text(
  315. l10n.appSubtitle,
  316. style: const TextStyle(
  317. fontSize: 16,
  318. color: Color(0xFF6B7280),
  319. ),
  320. textAlign: TextAlign.center,
  321. ),
  322. const SizedBox(height: 48),
  323. // Tab 切换
  324. Container(
  325. decoration: BoxDecoration(
  326. color: Colors.white,
  327. borderRadius: BorderRadius.circular(12),
  328. ),
  329. child: TabBar(
  330. controller: _tabController,
  331. indicator: BoxDecoration(
  332. borderRadius: BorderRadius.circular(12),
  333. color: const Color(0xFF00BFA5),
  334. ),
  335. indicatorSize: TabBarIndicatorSize.tab,
  336. dividerColor: Colors.transparent,
  337. labelColor: Colors.white,
  338. unselectedLabelColor: const Color(0xFF6B7280),
  339. labelStyle: const TextStyle(
  340. fontSize: 16,
  341. fontWeight: FontWeight.w600,
  342. ),
  343. unselectedLabelStyle: const TextStyle(
  344. fontSize: 16,
  345. fontWeight: FontWeight.w500,
  346. ),
  347. tabs: [
  348. Tab(text: l10n.passwordLogin),
  349. Tab(text: l10n.smsLogin),
  350. ],
  351. ),
  352. ),
  353. const SizedBox(height: 24),
  354. // Tab 内容
  355. SizedBox(
  356. height: 400,
  357. child: TabBarView(
  358. controller: _tabController,
  359. children: [
  360. _buildPasswordLoginForm(),
  361. _buildSmsLoginForm(),
  362. ],
  363. ),
  364. ),
  365. ],
  366. ),
  367. ),
  368. ),
  369. ),
  370. );
  371. }
  372. // 密码登录表单
  373. Widget _buildPasswordLoginForm() {
  374. final l10n = AppLocalizations.of(context)!;
  375. return Form(
  376. key: _passwordFormKey,
  377. child: Column(
  378. crossAxisAlignment: CrossAxisAlignment.stretch,
  379. children: [
  380. const SizedBox(height: 4),
  381. // 手机号输入
  382. TextFormField(
  383. controller: _phoneController,
  384. keyboardType: TextInputType.phone,
  385. decoration: InputDecoration(
  386. labelText: l10n.phoneNumber,
  387. hintText: l10n.phoneNumberRequired,
  388. prefixIcon: const Icon(Icons.phone_outlined),
  389. ),
  390. validator: (value) {
  391. if (value == null || value.isEmpty) {
  392. return l10n.phoneNumberRequired;
  393. }
  394. if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
  395. return l10n.phoneNumberInvalid;
  396. }
  397. return null;
  398. },
  399. ),
  400. const SizedBox(height: 16),
  401. // 密码输入
  402. Consumer(
  403. builder: (context, ref, child) {
  404. final obscurePassword = ref.watch(passwordObscureProvider);
  405. return TextFormField(
  406. controller: _passwordController,
  407. obscureText: obscurePassword,
  408. decoration: InputDecoration(
  409. labelText: l10n.password,
  410. hintText: l10n.passwordRequired,
  411. prefixIcon: const Icon(Icons.lock_outline),
  412. suffixIcon: IconButton(
  413. icon: Icon(
  414. obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined,
  415. ),
  416. onPressed: () {
  417. ref.read(passwordObscureProvider.notifier).state = !obscurePassword;
  418. },
  419. ),
  420. ),
  421. validator: (value) {
  422. if (value == null || value.isEmpty) {
  423. return l10n.passwordRequired;
  424. }
  425. if (value.length < AppConstants.passwordMinLength) {
  426. return l10n.passwordMinLength;
  427. }
  428. return null;
  429. },
  430. );
  431. },
  432. ),
  433. const SizedBox(height: 8),
  434. // 记住密码 + 忘记密码
  435. Row(
  436. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  437. children: [
  438. // 记住密码复选框
  439. Consumer(
  440. builder: (context, ref, child) {
  441. final rememberPassword = ref.watch(rememberPasswordProvider);
  442. return Row(
  443. mainAxisSize: MainAxisSize.min,
  444. children: [
  445. Checkbox(
  446. value: rememberPassword,
  447. onChanged: (value) {
  448. ref.read(rememberPasswordProvider.notifier).state = value ?? false;
  449. },
  450. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  451. visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
  452. ),
  453. const SizedBox(width: 4),
  454. GestureDetector(
  455. onTap: () {
  456. ref.read(rememberPasswordProvider.notifier).state = !rememberPassword;
  457. },
  458. child: Text(
  459. l10n.rememberPassword,
  460. style: const TextStyle(
  461. fontSize: 14,
  462. color: Color(0xFF6B7280),
  463. ),
  464. ),
  465. ),
  466. ],
  467. );
  468. },
  469. ),
  470. // 忘记密码
  471. TextButton(
  472. onPressed: () {
  473. // 跳转到忘记密码页面
  474. if (mounted) {
  475. // 打开对话框前重置所有状态,确保每次打开都是干净的
  476. ref.read(changePwdPhoneProvider.notifier).state = '';
  477. ref.read(changePwdServerSmsCodeProvider.notifier).state = '';
  478. ref.read(changePwdPasswordProvider.notifier).state = '';
  479. ref.read(changePwdPasswordVisibleProvider.notifier).state = false;
  480. ref.read(changePwdConfirmPasswordProvider.notifier).state = '';
  481. ref.read(changePwdConfirmPasswordVisibleProvider.notifier).state = false;
  482. ref.read(changePwdSmsHasSentProvider.notifier).state = false;
  483. ref.read(changePwdSmsCountdownProvider.notifier).state = 0;
  484. ref.read(changePwdShowPasswordErrorProvider.notifier).state = false;
  485. showDialog(
  486. context: context,
  487. builder: (context) => const ChangePasswordPage(),
  488. );
  489. }
  490. },
  491. child: Text(l10n.forgotPassword),
  492. ),
  493. ],
  494. ),
  495. const SizedBox(height: 24),
  496. // 登录按钮
  497. ElevatedButton(
  498. onPressed: _handlePasswordLogin,
  499. style: ElevatedButton.styleFrom(
  500. padding: const EdgeInsets.symmetric(vertical: 16),
  501. ),
  502. child: Text(l10n.login),
  503. ),
  504. ],
  505. ),
  506. );
  507. }
  508. // 验证码登录表单
  509. Widget _buildSmsLoginForm() {
  510. final l10n = AppLocalizations.of(context)!;
  511. return Form(
  512. key: _smsFormKey,
  513. child: Column(
  514. crossAxisAlignment: CrossAxisAlignment.stretch,
  515. children: [
  516. const SizedBox(height: 4),
  517. // 手机号输入
  518. TextFormField(
  519. controller: _phoneSmsController,
  520. keyboardType: TextInputType.phone,
  521. decoration: InputDecoration(
  522. labelText: l10n.phoneNumber,
  523. hintText: l10n.phoneNumberRequired,
  524. prefixIcon: const Icon(Icons.phone_outlined),
  525. ),
  526. validator: (value) {
  527. if (value == null || value.isEmpty) {
  528. return l10n.phoneNumberRequired;
  529. }
  530. if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
  531. return l10n.phoneNumberInvalid;
  532. }
  533. return null;
  534. },
  535. ),
  536. const SizedBox(height: 16),
  537. // 验证码输入
  538. Row(
  539. children: [
  540. Expanded(
  541. child: TextFormField(
  542. controller: _smsCodeController,
  543. keyboardType: TextInputType.number,
  544. decoration: InputDecoration(
  545. labelText: l10n.smsCode,
  546. hintText: l10n.smsCodeRequired,
  547. prefixIcon: const Icon(Icons.sms_outlined),
  548. ),
  549. validator: (value) {
  550. // 验证码为空
  551. if (value == null || value.isEmpty) {
  552. return l10n.smsCodeRequired;
  553. }
  554. // 验证码格式不正确(长度或格式)
  555. if (value.length != AppConstants.smsCodeLength || !RegExp(r'^\d+$').hasMatch(value)) {
  556. return l10n.smsCodeInvalid;
  557. }
  558. return null;
  559. },
  560. ),
  561. ),
  562. const SizedBox(width: 12),
  563. Consumer(
  564. builder: (context, ref, child) {
  565. final countdown = ref.watch(smsCountdownProvider);
  566. final hasReceived = ref.watch(smsHasReceivedProvider);
  567. final serverSmsCode = ref.watch(smsCodeFromServerProvider);
  568. // 验证码已过期:曾经获取过验证码,但现在验证码为空且倒计时为0
  569. final isExpired = hasReceived && serverSmsCode.isEmpty && countdown == 0;
  570. // 调试日志
  571. if (countdown == 0 && hasReceived) {
  572. AppLogger.d('按钮状态 - countdown: $countdown, hasReceived: $hasReceived, serverSmsCode: $serverSmsCode, isExpired: $isExpired');
  573. }
  574. return SizedBox(
  575. width: 100,
  576. child: ElevatedButton(
  577. onPressed: countdown > 0 ? null : _sendSmsCode,
  578. style: ElevatedButton.styleFrom(
  579. padding: const EdgeInsets.symmetric(vertical: 16),
  580. backgroundColor: countdown > 0
  581. ? const Color(0xFFE5E7EB)
  582. : const Color(0xFF00BFA5),
  583. ),
  584. child: Text(
  585. countdown > 0
  586. ? l10n.smsCodeCountdown(countdown)
  587. : (isExpired ? l10n.resendSmsCode : l10n.getSmsCode),
  588. style: TextStyle(
  589. color: countdown > 0
  590. ? const Color(0xFF6B7280)
  591. : Colors.white,
  592. fontSize: isExpired ? 12 : 14,
  593. ),
  594. ),
  595. ),
  596. );
  597. },
  598. ),
  599. ],
  600. ),
  601. const SizedBox(height: 24),
  602. // 登录按钮
  603. ElevatedButton(
  604. onPressed: _handleSmsLogin,
  605. style: ElevatedButton.styleFrom(
  606. padding: const EdgeInsets.symmetric(vertical: 16),
  607. ),
  608. child: Text(l10n.login),
  609. ),
  610. ],
  611. ),
  612. );
  613. }
  614. }