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:
hailin 2026-03-05 05:51:47 -08:00
parent 8de92f2511
commit 41fa6349bd
3 changed files with 369 additions and 2 deletions

View File

@ -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;
}
}
// ============ ============
///

View File

@ -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);

View File

@ -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,
),
),
),
),
),
),
);
}
}