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 '../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<PlantingLocationPage> {
|
|||
province: _selectedProvinceName!,
|
||||
city: _selectedCityName!,
|
||||
skipCountdown: true, // 跳过倒计时
|
||||
onConfirm: _submitPlanting,
|
||||
onConfirm: _verifyPasswordThenSubmit,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +208,7 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
|||
province: _selectedProvinceName!,
|
||||
city: _selectedCityName!,
|
||||
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 {
|
||||
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