feat(mining-app): 添加找回密码和修改密码功能
- 添加 /password/reset 和 /password/change API 端点 - 在 auth_remote_datasource 中实现 resetPassword 和 changePassword 方法 - 在 user_providers 中添加状态管理方法 - 创建找回密码页面 (forgot_password_page.dart) - 创建修改密码页面 (change_password_page.dart) - 添加路由配置 - 在登录页面添加"忘记密码"链接 - 在个人资料页面添加"修改密码"入口 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
36d7b7ebfe
commit
26dce24e75
|
|
@ -8,6 +8,8 @@ class ApiEndpoints {
|
||||||
static const String refreshToken = '/auth/refresh';
|
static const String refreshToken = '/auth/refresh';
|
||||||
static const String logout = '/auth/logout';
|
static const String logout = '/auth/logout';
|
||||||
static const String userProfile = '/user/profile';
|
static const String userProfile = '/user/profile';
|
||||||
|
static const String resetPassword = '/password/reset';
|
||||||
|
static const String changePassword = '/password/change';
|
||||||
|
|
||||||
// Mining Service
|
// Mining Service
|
||||||
static String shareAccount(String accountSequence) => '/api/v1/accounts/$accountSequence';
|
static String shareAccount(String accountSequence) => '/api/v1/accounts/$accountSequence';
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'package:go_router/go_router.dart';
|
||||||
import '../../presentation/pages/splash/splash_page.dart';
|
import '../../presentation/pages/splash/splash_page.dart';
|
||||||
import '../../presentation/pages/auth/login_page.dart';
|
import '../../presentation/pages/auth/login_page.dart';
|
||||||
import '../../presentation/pages/auth/register_page.dart';
|
import '../../presentation/pages/auth/register_page.dart';
|
||||||
|
import '../../presentation/pages/auth/forgot_password_page.dart';
|
||||||
|
import '../../presentation/pages/auth/change_password_page.dart';
|
||||||
import '../../presentation/pages/home/home_page.dart';
|
import '../../presentation/pages/home/home_page.dart';
|
||||||
import '../../presentation/pages/contribution/contribution_page.dart';
|
import '../../presentation/pages/contribution/contribution_page.dart';
|
||||||
import '../../presentation/pages/trading/trading_page.dart';
|
import '../../presentation/pages/trading/trading_page.dart';
|
||||||
|
|
@ -26,6 +28,14 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
path: Routes.register,
|
path: Routes.register,
|
||||||
builder: (context, state) => const RegisterPage(),
|
builder: (context, state) => const RegisterPage(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.forgotPassword,
|
||||||
|
builder: (context, state) => const ForgotPasswordPage(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.changePassword,
|
||||||
|
builder: (context, state) => const ChangePasswordPage(),
|
||||||
|
),
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) => MainShell(child: child),
|
builder: (context, state, child) => MainShell(child: child),
|
||||||
routes: [
|
routes: [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ class Routes {
|
||||||
static const String splash = '/';
|
static const String splash = '/';
|
||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
static const String register = '/register';
|
static const String register = '/register';
|
||||||
|
static const String forgotPassword = '/forgot-password';
|
||||||
|
static const String changePassword = '/change-password';
|
||||||
static const String home = '/home';
|
static const String home = '/home';
|
||||||
static const String contribution = '/contribution';
|
static const String contribution = '/contribution';
|
||||||
static const String trading = '/trading';
|
static const String trading = '/trading';
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ abstract class AuthRemoteDataSource {
|
||||||
Future<String> refreshToken(String refreshToken);
|
Future<String> refreshToken(String refreshToken);
|
||||||
Future<void> logout(String refreshToken);
|
Future<void> logout(String refreshToken);
|
||||||
Future<UserInfo> getProfile();
|
Future<UserInfo> getProfile();
|
||||||
|
Future<void> resetPassword(String phone, String smsCode, String newPassword);
|
||||||
|
Future<void> changePassword(String oldPassword, String newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||||
|
|
@ -162,4 +164,28 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||||
throw ServerException(e.toString());
|
throw ServerException(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> resetPassword(String phone, String smsCode, String newPassword) async {
|
||||||
|
try {
|
||||||
|
await client.post(
|
||||||
|
ApiEndpoints.resetPassword,
|
||||||
|
data: {'phone': phone, 'smsCode': smsCode, 'newPassword': newPassword},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> changePassword(String oldPassword, String newPassword) async {
|
||||||
|
try {
|
||||||
|
await client.post(
|
||||||
|
ApiEndpoints.changePassword,
|
||||||
|
data: {'oldPassword': oldPassword, 'newPassword': newPassword},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../core/constants/app_colors.dart';
|
||||||
|
import '../../providers/user_providers.dart';
|
||||||
|
|
||||||
|
class ChangePasswordPage extends ConsumerStatefulWidget {
|
||||||
|
const ChangePasswordPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ChangePasswordPage> createState() => _ChangePasswordPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangePasswordPageState extends ConsumerState<ChangePasswordPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _oldPasswordController = TextEditingController();
|
||||||
|
final _newPasswordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
|
||||||
|
bool _obscureOldPassword = true;
|
||||||
|
bool _obscureNewPassword = true;
|
||||||
|
bool _obscureConfirmPassword = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_oldPasswordController.dispose();
|
||||||
|
_newPasswordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _changePassword() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
final oldPassword = _oldPasswordController.text;
|
||||||
|
final newPassword = _newPasswordController.text;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(userNotifierProvider.notifier).changePassword(oldPassword, newPassword);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('密码修改成功')),
|
||||||
|
);
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('修改失败: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userState = ref.watch(userNotifierProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'修改密码',
|
||||||
|
style: TextStyle(color: Colors.black),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 当前密码
|
||||||
|
TextFormField(
|
||||||
|
controller: _oldPasswordController,
|
||||||
|
obscureText: _obscureOldPassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '当前密码',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureOldPassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscureOldPassword = !_obscureOldPassword),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入当前密码';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 新密码
|
||||||
|
TextFormField(
|
||||||
|
controller: _newPasswordController,
|
||||||
|
obscureText: _obscureNewPassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '新密码',
|
||||||
|
prefixIcon: const Icon(Icons.lock),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureNewPassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscureNewPassword = !_obscureNewPassword),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入新密码';
|
||||||
|
}
|
||||||
|
if (value.length < 6) {
|
||||||
|
return '密码至少6位';
|
||||||
|
}
|
||||||
|
if (value == _oldPasswordController.text) {
|
||||||
|
return '新密码不能与当前密码相同';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 确认新密码
|
||||||
|
TextFormField(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
obscureText: _obscureConfirmPassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '确认新密码',
|
||||||
|
prefixIcon: const Icon(Icons.lock),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请确认新密码';
|
||||||
|
}
|
||||||
|
if (value != _newPasswordController.text) {
|
||||||
|
return '两次密码输入不一致';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 修改密码按钮
|
||||||
|
SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: userState.isLoading ? null : _changePassword,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: userState.isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'确认修改',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, size: 20, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'密码要求',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'• 密码长度至少6位\n• 建议使用字母、数字和符号的组合\n• 新密码不能与当前密码相同',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../core/router/routes.dart';
|
||||||
|
import '../../../core/constants/app_colors.dart';
|
||||||
|
import '../../providers/user_providers.dart';
|
||||||
|
|
||||||
|
class ForgotPasswordPage extends ConsumerStatefulWidget {
|
||||||
|
const ForgotPasswordPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _phoneController = TextEditingController();
|
||||||
|
final _smsCodeController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
|
||||||
|
bool _obscurePassword = true;
|
||||||
|
bool _obscureConfirmPassword = true;
|
||||||
|
int _countDown = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_phoneController.dispose();
|
||||||
|
_smsCodeController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendSmsCode() async {
|
||||||
|
final phone = _phoneController.text.trim();
|
||||||
|
if (phone.isEmpty || !RegExp(r'^1[3-9]\d{9}$').hasMatch(phone)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('请输入正确的手机号')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(userNotifierProvider.notifier).sendSmsCode(phone, 'RESET_PASSWORD');
|
||||||
|
setState(() => _countDown = 60);
|
||||||
|
_startCountDown();
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('验证码已发送')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('发送失败: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startCountDown() {
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
if (_countDown > 0 && mounted) {
|
||||||
|
setState(() => _countDown--);
|
||||||
|
_startCountDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _resetPassword() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
final phone = _phoneController.text.trim();
|
||||||
|
final smsCode = _smsCodeController.text.trim();
|
||||||
|
final newPassword = _passwordController.text;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(userNotifierProvider.notifier).resetPassword(phone, smsCode, newPassword);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('密码重置成功,请登录')),
|
||||||
|
);
|
||||||
|
context.go(Routes.login);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('重置失败: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userState = ref.watch(userNotifierProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'找回密码',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'请输入您的手机号,我们将发送验证码帮您重置密码',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 手机号
|
||||||
|
TextFormField(
|
||||||
|
controller: _phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '手机号',
|
||||||
|
prefixIcon: const Icon(Icons.phone),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入手机号';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
|
||||||
|
return '请输入正确的手机号';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 验证码
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _smsCodeController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 6,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '验证码',
|
||||||
|
prefixIcon: const Icon(Icons.sms),
|
||||||
|
counterText: '',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入验证码';
|
||||||
|
}
|
||||||
|
if (value.length != 6) {
|
||||||
|
return '验证码为6位';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _countDown > 0 ? null : _sendSmsCode,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_countDown > 0 ? '${_countDown}s' : '获取验证码',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 新密码
|
||||||
|
TextFormField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: _obscurePassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '新密码',
|
||||||
|
prefixIcon: const Icon(Icons.lock),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入新密码';
|
||||||
|
}
|
||||||
|
if (value.length < 6) {
|
||||||
|
return '密码至少6位';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 确认新密码
|
||||||
|
TextFormField(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
obscureText: _obscureConfirmPassword,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '确认新密码',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请确认新密码';
|
||||||
|
}
|
||||||
|
if (value != _passwordController.text) {
|
||||||
|
return '两次密码输入不一致';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 重置密码按钮
|
||||||
|
SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: userState.isLoading ? null : _resetPassword,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: userState.isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'重置密码',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 返回登录
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'想起密码了?',
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: const Text('返回登录'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -338,7 +338,22 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 忘记密码
|
||||||
|
if (_isPasswordLogin)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => context.push(Routes.forgotPassword),
|
||||||
|
child: Text(
|
||||||
|
'忘记密码?',
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 注册入口
|
// 注册入口
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../core/constants/app_colors.dart';
|
import '../../../core/constants/app_colors.dart';
|
||||||
|
import '../../../core/router/routes.dart';
|
||||||
import '../../providers/user_providers.dart';
|
import '../../providers/user_providers.dart';
|
||||||
|
|
||||||
class ProfilePage extends ConsumerWidget {
|
class ProfilePage extends ConsumerWidget {
|
||||||
|
|
@ -94,6 +96,11 @@ class ProfilePage extends ConsumerWidget {
|
||||||
label: '个人信息',
|
label: '个人信息',
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
|
_MenuItem(
|
||||||
|
icon: Icons.lock_outline,
|
||||||
|
label: '修改密码',
|
||||||
|
onTap: () => context.push(Routes.changePassword),
|
||||||
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: Icons.security,
|
icon: Icons.security,
|
||||||
label: '安全设置',
|
label: '安全设置',
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,28 @@ class UserNotifier extends StateNotifier<UserState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> resetPassword(String phone, String smsCode, String newPassword) async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
try {
|
||||||
|
await _authDataSource.resetPassword(phone, smsCode, newPassword);
|
||||||
|
state = state.copyWith(isLoading: false);
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changePassword(String oldPassword, String newPassword) async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
try {
|
||||||
|
await _authDataSource.changePassword(oldPassword, newPassword);
|
||||||
|
state = state.copyWith(isLoading: false);
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Legacy method for compatibility
|
// Legacy method for compatibility
|
||||||
void login({
|
void login({
|
||||||
required String accountSequence,
|
required String accountSequence,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue