feat(mobile-app): 认种确认前新增登录密码校验弹窗
- 新增 PasswordVerifyDialog 弹窗,风格与主体 App 一致 - account_service 新增 verifyLoginPassword() 调用 POST /user/verify-password - planting_location_page 在 PlantingConfirmDialog 确认后插入密码校验步骤 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8de92f2511
commit
41fa6349bd
|
|
@ -2095,6 +2095,27 @@ class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 验证登录密码 (POST /user/verify-password)
|
||||||
|
///
|
||||||
|
/// 返回 true 表示密码正确,false 表示密码错误,抛出异常表示网络/系统错误
|
||||||
|
Future<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 邮箱绑定相关 ============
|
// ============ 邮箱绑定相关 ============
|
||||||
|
|
||||||
/// 获取邮箱绑定状态
|
/// 获取邮箱绑定状态
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:city_pickers/city_pickers.dart';
|
import 'package:city_pickers/city_pickers.dart';
|
||||||
import '../widgets/planting_confirm_dialog.dart';
|
import '../widgets/planting_confirm_dialog.dart';
|
||||||
import '../widgets/kyc_required_dialog.dart';
|
import '../widgets/kyc_required_dialog.dart';
|
||||||
|
import '../widgets/password_verify_dialog.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
import '../../../../core/storage/storage_keys.dart';
|
import '../../../../core/storage/storage_keys.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
|
|
@ -111,7 +112,7 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
||||||
province: _selectedProvinceName!,
|
province: _selectedProvinceName!,
|
||||||
city: _selectedCityName!,
|
city: _selectedCityName!,
|
||||||
skipCountdown: true, // 跳过倒计时
|
skipCountdown: true, // 跳过倒计时
|
||||||
onConfirm: _submitPlanting,
|
onConfirm: _verifyPasswordThenSubmit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,7 +208,7 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
||||||
province: _selectedProvinceName!,
|
province: _selectedProvinceName!,
|
||||||
city: _selectedCityName!,
|
city: _selectedCityName!,
|
||||||
skipCountdown: _hasSavedLocation,
|
skipCountdown: _hasSavedLocation,
|
||||||
onConfirm: _submitPlanting,
|
onConfirm: _verifyPasswordThenSubmit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,6 +243,20 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 验证密码后再提交认种
|
||||||
|
Future<void> _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<void> _submitPlanting() async {
|
Future<void> _submitPlanting() async {
|
||||||
setState(() => _isSubmitting = true);
|
setState(() => _isSubmitting = true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,331 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 认种密码验证回调类型
|
||||||
|
/// 返回 true 表示密码正确,false 表示密码错误,抛出异常表示网络/系统错误
|
||||||
|
typedef PasswordVerifyCallback = Future<bool> Function(String password);
|
||||||
|
|
||||||
|
/// 认种密码校验弹窗
|
||||||
|
/// 在用户确认认种前,要求输入登录密码进行身份验证
|
||||||
|
class PasswordVerifyDialog extends StatefulWidget {
|
||||||
|
final PasswordVerifyCallback onVerify;
|
||||||
|
|
||||||
|
const PasswordVerifyDialog({
|
||||||
|
super.key,
|
||||||
|
required this.onVerify,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 显示密码验证弹窗
|
||||||
|
/// 返回 true 表示验证通过,false/null 表示用户取消
|
||||||
|
static Future<bool?> show({
|
||||||
|
required BuildContext context,
|
||||||
|
required PasswordVerifyCallback onVerify,
|
||||||
|
}) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
barrierColor: const Color(0x80000000),
|
||||||
|
builder: (context) => PasswordVerifyDialog(onVerify: onVerify),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PasswordVerifyDialog> createState() => _PasswordVerifyDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
|
||||||
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
|
||||||
|
bool _showPassword = false;
|
||||||
|
bool _isVerifying = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 点击确认按钮
|
||||||
|
Future<void> _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<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'确认',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.5,
|
||||||
|
letterSpacing: 0.24,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue