diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index caae8919..00f67a3a 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -2095,6 +2095,27 @@ class AccountService { } } + /// 验证登录密码 (POST /user/verify-password) + /// + /// 返回 true 表示密码正确,false 表示密码错误,抛出异常表示网络/系统错误 + Future verifyLoginPassword(String password) async { + debugPrint('$_tag verifyLoginPassword() - 开始验证登录密码'); + try { + await _apiClient.post( + '/user/verify-password', + data: {'password': password}, + ); + debugPrint('$_tag verifyLoginPassword() - 密码验证成功'); + return true; + } on ApiException catch (e) { + debugPrint('$_tag verifyLoginPassword() - 密码错误: $e'); + return false; + } catch (e) { + debugPrint('$_tag verifyLoginPassword() - 验证异常: $e'); + rethrow; + } + } + // ============ 邮箱绑定相关 ============ /// 获取邮箱绑定状态 diff --git a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart index 06a4ea24..5dbe79f1 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:city_pickers/city_pickers.dart'; import '../widgets/planting_confirm_dialog.dart'; import '../widgets/kyc_required_dialog.dart'; +import '../widgets/password_verify_dialog.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/storage/storage_keys.dart'; import '../../../../routes/route_paths.dart'; @@ -111,7 +112,7 @@ class _PlantingLocationPageState extends ConsumerState { province: _selectedProvinceName!, city: _selectedCityName!, skipCountdown: true, // 跳过倒计时 - onConfirm: _submitPlanting, + onConfirm: _verifyPasswordThenSubmit, ); } @@ -207,7 +208,7 @@ class _PlantingLocationPageState extends ConsumerState { province: _selectedProvinceName!, city: _selectedCityName!, skipCountdown: _hasSavedLocation, - onConfirm: _submitPlanting, + onConfirm: _verifyPasswordThenSubmit, ); } @@ -242,6 +243,20 @@ class _PlantingLocationPageState extends ConsumerState { } } + /// 验证密码后再提交认种 + Future _verifyPasswordThenSubmit() async { + final verified = await PasswordVerifyDialog.show( + context: context, + onVerify: (password) async { + final accountService = ref.read(accountServiceProvider); + return await accountService.verifyLoginPassword(password); + }, + ); + if (verified == true) { + await _submitPlanting(); + } + } + /// 提交认种请求 Future _submitPlanting() async { setState(() => _isSubmitting = true); diff --git a/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart b/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart new file mode 100644 index 00000000..9de78291 --- /dev/null +++ b/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart @@ -0,0 +1,331 @@ +import 'package:flutter/material.dart'; + +/// 认种密码验证回调类型 +/// 返回 true 表示密码正确,false 表示密码错误,抛出异常表示网络/系统错误 +typedef PasswordVerifyCallback = Future Function(String password); + +/// 认种密码校验弹窗 +/// 在用户确认认种前,要求输入登录密码进行身份验证 +class PasswordVerifyDialog extends StatefulWidget { + final PasswordVerifyCallback onVerify; + + const PasswordVerifyDialog({ + super.key, + required this.onVerify, + }); + + /// 显示密码验证弹窗 + /// 返回 true 表示验证通过,false/null 表示用户取消 + static Future show({ + required BuildContext context, + required PasswordVerifyCallback onVerify, + }) { + return showDialog( + context: context, + barrierDismissible: false, + barrierColor: const Color(0x80000000), + builder: (context) => PasswordVerifyDialog(onVerify: onVerify), + ); + } + + @override + State createState() => _PasswordVerifyDialogState(); +} + +class _PasswordVerifyDialogState extends State { + final TextEditingController _passwordController = TextEditingController(); + + bool _showPassword = false; + bool _isVerifying = false; + String? _errorMessage; + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + /// 点击确认按钮 + Future _handleConfirm() async { + final password = _passwordController.text.trim(); + if (password.isEmpty) { + setState(() => _errorMessage = '请输入登录密码'); + return; + } + + setState(() { + _isVerifying = true; + _errorMessage = null; + }); + + try { + final success = await widget.onVerify(password); + if (!mounted) return; + if (success) { + Navigator.pop(context, true); + } else { + setState(() { + _isVerifying = false; + _errorMessage = '密码错误,请重试'; + }); + } + } catch (e) { + if (!mounted) return; + setState(() { + _isVerifying = false; + _errorMessage = '验证失败,请检查网络后重试'; + }); + } + } + + /// 关闭弹窗 + void _handleClose() { + Navigator.pop(context, false); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 384), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x40000000), + blurRadius: 50, + offset: Offset(0, 25), + spreadRadius: -12, + ), + ], + ), + child: Stack( + children: [ + // 主内容 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildIcon(), + const SizedBox(height: 4), + _buildTitle(), + const SizedBox(height: 8), + _buildSubtitle(), + const SizedBox(height: 20), + _buildPasswordInput(), + if (_errorMessage != null) ...[ + const SizedBox(height: 8), + _buildErrorMessage(), + ], + const SizedBox(height: 20), + _buildConfirmButton(), + ], + ), + ), + // 右上角关闭按钮 + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: _isVerifying ? null : _handleClose, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: const Color(0x0D000000), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.close, + size: 18, + color: Color(0xFF8B5A2B), + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// 构建锁图标 + Widget _buildIcon() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: const Color(0xFFFFF7E6), + borderRadius: BorderRadius.circular(30), + ), + child: const Center( + child: Icon( + Icons.lock_outline, + color: Color(0xFF8B5A2B), + size: 32, + ), + ), + ), + ); + } + + /// 构建标题 + Widget _buildTitle() { + return const Text( + '验证登录密码', + style: TextStyle( + fontSize: 20, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.3, + letterSpacing: -0.3, + color: Color(0xFF5D4037), + ), + textAlign: TextAlign.center, + ); + } + + /// 构建说明文字 + Widget _buildSubtitle() { + return const Text( + '为保障账户安全,请输入您的登录密码以继续认种', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF745D43), + ), + textAlign: TextAlign.center, + ); + } + + /// 构建密码输入框 + Widget _buildPasswordInput() { + final hasError = _errorMessage != null; + return Container( + width: double.infinity, + height: 52, + decoration: BoxDecoration( + color: const Color(0xFFFFF7E6), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: hasError + ? const Color(0xFFFF4D4F) + : const Color(0x4D8B5A2B), + width: 1, + ), + ), + child: TextField( + controller: _passwordController, + obscureText: !_showPassword, + enabled: !_isVerifying, + onChanged: (_) { + if (_errorMessage != null) { + setState(() => _errorMessage = null); + } + }, + style: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.19, + color: Color(0xFF5D4037), + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: InputBorder.none, + hintText: '请输入登录密码', + hintStyle: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.19, + color: Color(0x995D4037), + ), + suffixIcon: GestureDetector( + onTap: () => setState(() => _showPassword = !_showPassword), + child: Icon( + _showPassword ? Icons.visibility_off : Icons.visibility, + color: const Color(0xFF8B5A2B), + size: 20, + ), + ), + ), + ), + ); + } + + /// 构建错误提示 + Widget _buildErrorMessage() { + return Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + const Icon( + Icons.error_outline, + size: 14, + color: Color(0xFFFF4D4F), + ), + const SizedBox(width: 4), + Text( + _errorMessage!, + style: const TextStyle( + fontSize: 12, + fontFamily: 'Inter', + color: Color(0xFFFF4D4F), + ), + ), + ], + ), + ); + } + + /// 构建确认按钮 + Widget _buildConfirmButton() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + width: double.infinity, + child: GestureDetector( + onTap: _isVerifying ? null : _handleConfirm, + child: Container( + height: 48, + decoration: BoxDecoration( + color: _isVerifying + ? const Color(0xFFD4AF37).withValues(alpha: 0.5) + : const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: _isVerifying + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text( + '确认', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.5, + letterSpacing: 0.24, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ); + } +}