2025-12-12 09:11:30 +08:00
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/pages/my_appbar.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/pages/user/CustomInput.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/pages/user/login_page.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/tools/tools.dart';
|
|
|
|
|
|
import '../../http/ApiService.dart'; // 假设你的 API 在这里
|
|
|
|
|
|
|
|
|
|
|
|
class RegisterPage extends StatefulWidget {
|
|
|
|
|
|
const RegisterPage({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<RegisterPage> createState() => _RegisterPageState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _RegisterPageState extends State<RegisterPage> {
|
|
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
|
|
|
|
|
|
|
|
final TextEditingController _phoneController = TextEditingController();
|
|
|
|
|
|
final TextEditingController _codeController = TextEditingController();
|
|
|
|
|
|
final TextEditingController _pwdController = TextEditingController();
|
|
|
|
|
|
final TextEditingController _confirmPwdController = TextEditingController();
|
|
|
|
|
|
|
|
|
|
|
|
bool _obscurePwd = true;
|
|
|
|
|
|
bool _obscureConfirm = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码发送状态和倒计时
|
|
|
|
|
|
bool _isSendingCode = false;
|
|
|
|
|
|
int _secondsLeft = 0;
|
|
|
|
|
|
Timer? _timer;
|
|
|
|
|
|
|
|
|
|
|
|
String textString =
|
|
|
|
|
|
"*密码长度8-18位,必须包含大小写字母+数字+特殊字母,例如:Qa@123456";
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
_phoneController.dispose();
|
|
|
|
|
|
_codeController.dispose();
|
|
|
|
|
|
_pwdController.dispose();
|
|
|
|
|
|
_confirmPwdController.dispose();
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证密码复杂度
|
|
|
|
|
|
bool isPasswordValid(String password) {
|
|
|
|
|
|
final hasUpperCase = RegExp(r'[A-Z]');
|
|
|
|
|
|
final hasLowerCase = RegExp(r'[a-z]');
|
|
|
|
|
|
final hasNumber = RegExp(r'[0-9]');
|
|
|
|
|
|
final hasSpecialChar = RegExp(r'[!@#\$%\^&\*\(\)_\+\-=\[\]\{\};:"\\|,.<>\/\?~`]');
|
|
|
|
|
|
return hasUpperCase.hasMatch(password) &&
|
|
|
|
|
|
hasLowerCase.hasMatch(password) &&
|
|
|
|
|
|
hasNumber.hasMatch(password) &&
|
|
|
|
|
|
hasSpecialChar.hasMatch(password);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 手机号简单校验(11 位数字)
|
|
|
|
|
|
bool _isPhoneValid(String phone) {
|
|
|
|
|
|
final RegExp phoneReg = RegExp(r'^\d{11}$');
|
|
|
|
|
|
return phoneReg.hasMatch(phone);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _startCountdown(int seconds) {
|
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_secondsLeft = seconds;
|
|
|
|
|
|
});
|
|
|
|
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_secondsLeft--;
|
|
|
|
|
|
if (_secondsLeft <= 0) {
|
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
|
_secondsLeft = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _sendCode() async {
|
|
|
|
|
|
final phone = _phoneController.text.trim();
|
|
|
|
|
|
if (phone.isEmpty) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '请输入手机号');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!_isPhoneValid(phone)) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '请输入有效的手机号(11位)');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_isSendingCode || _secondsLeft > 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_isSendingCode = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
LoadingDialogHelper.show();
|
|
|
|
|
|
try {
|
|
|
|
|
|
final resp = await BasicInfoApi.sendRegisterSms({'phone': phone});
|
|
|
|
|
|
LoadingDialogHelper.hide();
|
|
|
|
|
|
|
|
|
|
|
|
if (resp != null && resp['success'] == true) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '验证码已发送');
|
|
|
|
|
|
_startCountdown(60);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ToastUtil.showNormal(context, resp?['message'] ?? '发送验证码失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
LoadingDialogHelper.hide();
|
|
|
|
|
|
ToastUtil.showNormal(context, '发送验证码失败,请稍后重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_isSendingCode = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _handleRegister() async {
|
|
|
|
|
|
if (!(_formKey.currentState?.validate() ?? false)) return;
|
|
|
|
|
|
|
|
|
|
|
|
final phone = _phoneController.text.trim();
|
|
|
|
|
|
final code = _codeController.text.trim();
|
|
|
|
|
|
final pwd = _pwdController.text.trim();
|
|
|
|
|
|
final confirm = _confirmPwdController.text.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!_isPhoneValid(phone)) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '请输入有效的手机号(11位)');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (code.isEmpty) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '请输入验证码');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pwd.isEmpty) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '请输入密码');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (confirm.isEmpty) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '请确认密码');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pwd != confirm) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '两次输入的密码不一致');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pwd.length < 8) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '密码长度需至少8位');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pwd.length > 32) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '密码长度需小于32位');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isPasswordValid(pwd)) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '密码必须包含大小写字母、数字和特殊符号');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
final data = {
|
|
|
|
|
|
'phone': phone,
|
|
|
|
|
|
'phoneCode': code,
|
|
|
|
|
|
'newPassword': pwd,
|
|
|
|
|
|
'confirmPassword': pwd,
|
|
|
|
|
|
};
|
2025-12-24 16:07:53 +08:00
|
|
|
|
LoadingDialogHelper.show();
|
2025-12-12 09:11:30 +08:00
|
|
|
|
final resp = await BasicInfoApi.register(data);
|
2025-12-24 16:07:53 +08:00
|
|
|
|
LoadingDialogHelper.hide();
|
2025-12-12 09:11:30 +08:00
|
|
|
|
|
|
|
|
|
|
if (resp != null && resp['success'] == true) {
|
|
|
|
|
|
ToastUtil.showNormal(context, '注册成功,请登录');
|
|
|
|
|
|
// 跳转到登录页并移除当前页面栈
|
|
|
|
|
|
Navigator.pushAndRemoveUntil(
|
|
|
|
|
|
context,
|
|
|
|
|
|
MaterialPageRoute(builder: (context) => const LoginPage()),
|
|
|
|
|
|
(route) => false,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ToastUtil.showNormal(context, resp?['message'] ?? '注册失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2025-12-24 16:07:53 +08:00
|
|
|
|
LoadingDialogHelper.hide();
|
2025-12-12 09:11:30 +08:00
|
|
|
|
ToastUtil.showNormal(context, '注册失败,请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
appBar: MyAppbar(title: '注册账号'),
|
|
|
|
|
|
backgroundColor: Colors.white,
|
|
|
|
|
|
body: SafeArea(
|
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
|
|
|
|
|
child: Form(
|
|
|
|
|
|
key: _formKey,
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
|
|
|
|
|
|
// 手机号(使用 CustomInput)
|
|
|
|
|
|
CustomInput.buildInput(
|
|
|
|
|
|
_phoneController,
|
|
|
|
|
|
title: '手机号',
|
|
|
|
|
|
hint: '请输入手机号',
|
|
|
|
|
|
keyboardType: TextInputType.phone,
|
|
|
|
|
|
validator: (v) {
|
|
|
|
|
|
if (v == null || v.isEmpty) return '请输入手机号';
|
|
|
|
|
|
if (!_isPhoneValid(v.trim())) return '请输入有效的手机号';
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码 + 发送按钮 行(验证码输入使用 CustomInput)
|
|
|
|
|
|
Row(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: CustomInput.buildInput(
|
|
|
|
|
|
_codeController,
|
|
|
|
|
|
title: '验证码',
|
|
|
|
|
|
hint: '请输入验证码',
|
|
|
|
|
|
keyboardType: TextInputType.number,
|
|
|
|
|
|
validator: (v) {
|
|
|
|
|
|
if (v == null || v.isEmpty) return '请输入验证码';
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
|
Column(
|
|
|
|
|
|
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const SizedBox(height: 40),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
|
onPressed: (_secondsLeft > 0 || _isSendingCode) ? null : _sendCode,
|
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
|
backgroundColor: (_secondsLeft > 0) ? Colors.grey.shade400 : const Color(0xFF2A75F8),
|
|
|
|
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
_secondsLeft > 0 ? '$_secondsLeft s后可重发' : '发送验证码',
|
|
|
|
|
|
style: const TextStyle(fontSize: 14, color: Colors.white),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
|
|
// 密码
|
|
|
|
|
|
CustomInput.buildInput(
|
|
|
|
|
|
_pwdController,
|
|
|
|
|
|
title: '密码',
|
|
|
|
|
|
hint: '请输入密码',
|
|
|
|
|
|
obscure: _obscurePwd,
|
|
|
|
|
|
suffix: IconButton(
|
|
|
|
|
|
icon: Icon(_obscurePwd ? Icons.visibility_off : Icons.visibility, color: Colors.grey),
|
|
|
|
|
|
onPressed: () => setState(() => _obscurePwd = !_obscurePwd),
|
|
|
|
|
|
),
|
|
|
|
|
|
validator: (v) {
|
|
|
|
|
|
if (v == null || v.isEmpty) return '请输入密码';
|
|
|
|
|
|
if (v.length < 8) return '密码长度至少 8 位';
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
|
|
// 确认密码
|
|
|
|
|
|
CustomInput.buildInput(
|
|
|
|
|
|
_confirmPwdController,
|
|
|
|
|
|
title: '确认密码',
|
|
|
|
|
|
hint: '请再次输入密码',
|
|
|
|
|
|
obscure: _obscureConfirm,
|
|
|
|
|
|
suffix: IconButton(
|
|
|
|
|
|
icon: Icon(_obscureConfirm ? Icons.visibility_off : Icons.visibility, color: Colors.grey),
|
|
|
|
|
|
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
|
|
|
|
|
|
),
|
|
|
|
|
|
validator: (v) {
|
|
|
|
|
|
if (v == null || v.isEmpty) return '请确认密码';
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
|
|
|
|
|
|
// 密码提示语
|
|
|
|
|
|
Align(
|
|
|
|
|
|
alignment: Alignment.centerLeft,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
textString,
|
|
|
|
|
|
style: const TextStyle(color: Colors.red, fontSize: 13),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
|
|
|
|
|
|
|
|
// 注册确认按钮
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
height: 45,
|
|
|
|
|
|
child: CustomButton(
|
|
|
|
|
|
onPressed: _handleRegister,
|
|
|
|
|
|
text: '确认',
|
|
|
|
|
|
backgroundColor: Colors.blue,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|