login_register_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import 'package:flutter_screenutil/flutter_screenutil.dart';
  6. import 'package:go_router/go_router.dart';
  7. import 'package:news_app/main.dart';
  8. import 'package:news_app/util/shared_prefs_instance_util.dart';
  9. import 'package:news_app/util/toast_util.dart';
  10. import 'package:news_app/widget/my_txt.dart';
  11. import '../../constant/api_const.dart';
  12. import '../../constant/color_res.dart';
  13. import '../../constant/config.dart';
  14. import '../../http/http_util.dart';
  15. /// @author: bo.zeng
  16. /// @email: cnhbwds@gmail.com
  17. /// @date: 2025 2025/4/9 16:00
  18. /// @description:
  19. class LoginRegisterPage extends ConsumerStatefulWidget {
  20. const LoginRegisterPage({super.key});
  21. @override
  22. ConsumerState<LoginRegisterPage> createState() => _LoginRegisterPageState();
  23. }
  24. class _LoginRegisterPageState extends ConsumerState<LoginRegisterPage> {
  25. bool _isLogin = true;
  26. bool _agreeProtocol = false;
  27. Timer? _timer;
  28. int seconds = 60;
  29. bool usePasswordLogin = true;
  30. bool _obscureText1 = true; // 控制密码是否隐藏
  31. bool _obscureText2 = true; // 控制密码是否隐藏
  32. String _smsLabel = "获取验证码";
  33. final TextEditingController _phoneController = TextEditingController();
  34. final TextEditingController _passwordController = TextEditingController();
  35. final TextEditingController _password2Controller = TextEditingController();
  36. final TextEditingController _smsCodeController = TextEditingController();
  37. @override
  38. void dispose() {
  39. _phoneController.dispose();
  40. _passwordController.dispose();
  41. _password2Controller.dispose();
  42. _smsCodeController.dispose();
  43. _timer?.cancel();
  44. super.dispose();
  45. }
  46. @override
  47. Widget build(BuildContext context) {
  48. return Scaffold(
  49. backgroundColor: Colors.white,
  50. resizeToAvoidBottomInset: false,
  51. appBar: AppBar(
  52. backgroundColor: color5F59F7,
  53. systemOverlayStyle: SystemUiOverlayStyle(
  54. statusBarColor: color5F59F7,
  55. statusBarIconBrightness: Brightness.light, // 状态栏图标颜色
  56. ),
  57. ),
  58. body: Stack(
  59. children: [
  60. Positioned(
  61. child: GestureDetector(
  62. onTap: (){
  63. FocusScope.of(context).unfocus();
  64. },
  65. child: Container(
  66. height: 200.h,
  67. decoration: BoxDecoration(
  68. gradient: LinearGradient(
  69. colors: [color5F59F7, color6592FD, Colors.white],
  70. begin: Alignment.topCenter,
  71. end: Alignment.bottomCenter,
  72. ),
  73. ),
  74. ),
  75. ),
  76. ),
  77. Padding(
  78. padding: EdgeInsets.only(left: 20.w, right: 20.w, top: 40.h),
  79. child: Column(
  80. crossAxisAlignment: CrossAxisAlignment.stretch,
  81. children: [
  82. // 登录/注册标签
  83. Row(
  84. spacing: 30.w,
  85. children: [
  86. GestureDetector(
  87. onTap: _switchLoginOrRegister,
  88. child: Column(
  89. children: [
  90. myTxt(
  91. text: "登录",
  92. fontSize: 20.sp,
  93. fontWeight: FontWeight.bold,
  94. ),
  95. Container(
  96. height: 4.h,
  97. width: 40.w,
  98. decoration: BoxDecoration(
  99. color:
  100. _isLogin ? Colors.black : Colors.transparent,
  101. borderRadius: BorderRadius.circular(8.r),
  102. ),
  103. ),
  104. ],
  105. ),
  106. ),
  107. GestureDetector(
  108. onTap: _switchLoginOrRegister,
  109. child: Column(
  110. children: [
  111. myTxt(
  112. text: "注册",
  113. fontSize: 20.sp,
  114. fontWeight: FontWeight.bold,
  115. ),
  116. Container(
  117. height: 4.h,
  118. width: 40.w,
  119. decoration: BoxDecoration(
  120. color:
  121. _isLogin ? Colors.transparent : Colors.black,
  122. borderRadius: BorderRadius.circular(8.r),
  123. ),
  124. ),
  125. ],
  126. ),
  127. ),
  128. ],
  129. ),
  130. SizedBox(height: 24.h),
  131. GestureDetector(
  132. onTap: (){
  133. FocusScope.of(context).unfocus();
  134. },
  135. child: Container(
  136. padding: EdgeInsets.symmetric(
  137. horizontal: 16.w,
  138. vertical: 20.h,
  139. ),
  140. decoration: BoxDecoration(
  141. color: Colors.white,
  142. borderRadius: BorderRadius.circular(8.r),
  143. ),
  144. alignment: Alignment.center,
  145. child: Column(
  146. spacing: 16.h,
  147. crossAxisAlignment: CrossAxisAlignment.start,
  148. children: [
  149. myTxt(text: "帐号", fontSize: 14.sp),
  150. TextField(
  151. textInputAction: TextInputAction.next,
  152. keyboardType: TextInputType.phone,
  153. controller: _phoneController,
  154. decoration: InputDecoration(
  155. prefixIcon: Icon(
  156. Icons.phone_android,
  157. color: Colors.grey,
  158. ),
  159. border: OutlineInputBorder(
  160. borderRadius: BorderRadius.circular(8.r),
  161. borderSide: BorderSide.none,
  162. ),
  163. filled: true,
  164. fillColor: colorF5F7FD,
  165. contentPadding: EdgeInsets.symmetric(
  166. horizontal: 16.w,
  167. vertical: 10.h,
  168. ),
  169. hintText: '请输入手机号',
  170. hintStyle: TextStyle(color: Colors.grey),
  171. ),
  172. ),
  173. if (_isLogin && !usePasswordLogin)
  174. myTxt(text: "验证码", fontSize: 14.sp),
  175. if (_isLogin && !usePasswordLogin)
  176. TextField(
  177. textInputAction: TextInputAction.done,
  178. keyboardType: TextInputType.number,
  179. controller: _smsCodeController,
  180. decoration: InputDecoration(
  181. prefixIcon: Icon(
  182. Icons.safety_check,
  183. color: Colors.grey,
  184. ),
  185. suffixIcon: Padding(
  186. padding: EdgeInsets.only(right: 10.w, top: 13.h),
  187. child: GestureDetector(
  188. onTap: _startSmsCodeTimer,
  189. child: myTxt(
  190. text: _smsLabel,
  191. color: color5F59F7,
  192. fontSize: 12.sp,
  193. fontWeight: FontWeight.bold,
  194. ),
  195. ),
  196. ),
  197. border: OutlineInputBorder(
  198. borderRadius: BorderRadius.circular(8.r),
  199. borderSide: BorderSide.none,
  200. ),
  201. filled: true,
  202. fillColor: colorF5F7FD,
  203. contentPadding: EdgeInsets.symmetric(
  204. horizontal: 16.w,
  205. vertical: 10.h,
  206. ),
  207. hintText: '请输入验证码',
  208. hintStyle: TextStyle(color: Colors.grey),
  209. ),
  210. ),
  211. if (usePasswordLogin || !_isLogin)
  212. myTxt(text: "密码", fontSize: 14.sp),
  213. if (usePasswordLogin || !_isLogin)
  214. TextField(
  215. obscureText: _obscureText1,
  216. controller: _passwordController,
  217. textInputAction: TextInputAction.done,
  218. keyboardType: TextInputType.text,
  219. decoration: InputDecoration(
  220. prefixIcon: Icon(
  221. Icons.lock_outline,
  222. color: Colors.grey,
  223. ),
  224. suffixIcon: IconButton(
  225. onPressed: () {
  226. setState(() {
  227. _obscureText1 = !_obscureText1;
  228. });
  229. },
  230. icon: Icon(
  231. _obscureText1
  232. ? Icons.visibility_off
  233. : Icons.visibility,
  234. color: Colors.grey,
  235. ),
  236. ),
  237. border: OutlineInputBorder(
  238. borderRadius: BorderRadius.circular(8.r),
  239. borderSide: BorderSide.none,
  240. ),
  241. filled: true,
  242. fillColor: colorF5F7FD,
  243. contentPadding: EdgeInsets.symmetric(
  244. horizontal: 16.w,
  245. vertical: 10.h,
  246. ),
  247. hintText: '请输入密码',
  248. hintStyle: TextStyle(color: Colors.grey),
  249. ),
  250. ),
  251. if (_isLogin)
  252. Center(
  253. child: TextButton(
  254. onPressed: () {
  255. setState(() {
  256. usePasswordLogin = !usePasswordLogin;
  257. });
  258. },
  259. child: myTxt(
  260. text: usePasswordLogin ? "使用验证码登录" : "使用密码登录",
  261. ),
  262. ),
  263. ),
  264. if (!_isLogin) myTxt(text: "确认密码", fontSize: 14.sp),
  265. if (!_isLogin)
  266. TextField(
  267. obscureText: _obscureText2,
  268. controller: _password2Controller,
  269. textInputAction: TextInputAction.done,
  270. keyboardType: TextInputType.text,
  271. decoration: InputDecoration(
  272. prefixIcon: Icon(
  273. Icons.lock_outline,
  274. color: Colors.grey,
  275. ),
  276. suffixIcon: IconButton(
  277. onPressed: () {
  278. setState(() {
  279. _obscureText2 = !_obscureText2;
  280. });
  281. },
  282. icon: Icon(
  283. _obscureText2
  284. ? Icons.visibility_off
  285. : Icons.visibility,
  286. color: Colors.grey,
  287. ),
  288. ),
  289. border: OutlineInputBorder(
  290. borderRadius: BorderRadius.circular(8.r),
  291. borderSide: BorderSide.none,
  292. ),
  293. filled: true,
  294. fillColor: colorF5F7FD,
  295. contentPadding: EdgeInsets.symmetric(
  296. horizontal: 16.w,
  297. vertical: 10.h,
  298. ),
  299. hintText: '请再输入密码',
  300. hintStyle: TextStyle(color: Colors.grey),
  301. ),
  302. ),
  303. // Align(
  304. // alignment: Alignment.centerRight,
  305. // child: myTxt(
  306. // text: "忘记密码",
  307. // color: color5F59F7,
  308. // fontSize: 12.sp,
  309. // fontWeight: FontWeight.bold,
  310. // ),
  311. // ),
  312. ],
  313. ),
  314. ),
  315. ),
  316. SizedBox(height: 16.h),
  317. _buildProtocolAgreement(),
  318. SizedBox(height: 24.h),
  319. // 登录/注册按钮
  320. GestureDetector(
  321. onTap: () {
  322. _submitForm(context, ref);
  323. },
  324. child: Container(
  325. margin: EdgeInsets.symmetric(horizontal: 28.w),
  326. height: 42.h,
  327. decoration: BoxDecoration(
  328. borderRadius: BorderRadius.circular(20.r),
  329. gradient: LinearGradient(
  330. colors: [color5F59F7, color6592FD],
  331. begin: Alignment.centerLeft,
  332. end: Alignment.centerRight,
  333. ),
  334. ),
  335. alignment: Alignment.center,
  336. child: myTxt(
  337. text: _isLogin ? "登录" : "注册",
  338. color: Colors.white,
  339. fontSize: 15.sp,
  340. ),
  341. ),
  342. ),
  343. SizedBox(height: 45.h),
  344. // Row(
  345. // spacing: 10.w,
  346. // children: [
  347. // Expanded(child: Container(height: 1, color: color979797)),
  348. // myTxt(text: "其他帐户登录", fontSize: 11.sp, color: color444D43),
  349. // Expanded(child: Container(height: 1, color: color979797)),
  350. // ],
  351. // ),
  352. SizedBox(height: 20.h),
  353. // Row(
  354. // spacing: 20.w,
  355. // mainAxisAlignment: MainAxisAlignment.center,
  356. // children: [
  357. // GestureDetector(
  358. // onTap: () {},
  359. // child: Image.asset(
  360. // Assets.images.wxIcon.path,
  361. // width: 52.w,
  362. // height: 52.w,
  363. // ),
  364. // ),
  365. // GestureDetector(
  366. // onTap: () {},
  367. // child: Image.asset(
  368. // Assets.images.wbIcon.path,
  369. // width: 52.w,
  370. // height: 52.w,
  371. // ),
  372. // ),
  373. // GestureDetector(
  374. // onTap: () {},
  375. // child: Image.asset(
  376. // Assets.images.qqIcon.path,
  377. // width: 52.w,
  378. // height: 52.w,
  379. // ),
  380. // ),
  381. // ],
  382. // ),
  383. ],
  384. ),
  385. ),
  386. ],
  387. ),
  388. );
  389. }
  390. void _switchLoginOrRegister() {
  391. setState(() {
  392. _isLogin = !_isLogin;
  393. });
  394. }
  395. void _startSmsCodeTimer() async {
  396. if (seconds != 60 || _timer?.isActive == true) {
  397. //防止重复触发
  398. return;
  399. }
  400. String mobile = _phoneController.text;
  401. if (mobile.isEmpty) {
  402. showToast("手机号不能为空");
  403. return;
  404. }
  405. await HttpUtil().post(apiSendLoginSMSCode, data: {"mobile": mobile});
  406. _timer = Timer.periodic(Duration(seconds: 1), (timer) {
  407. if (seconds > 0) {
  408. setState(() {
  409. seconds--;
  410. if (seconds >= 10) {
  411. _smsLabel = "${seconds}s后重发";
  412. } else {
  413. _smsLabel = "0${seconds}s后重发";
  414. }
  415. });
  416. } else {
  417. _smsLabel = "获取验证码";
  418. seconds = 60;
  419. _timer?.cancel();
  420. setState(() {});
  421. // 这里可以触发倒计时结束后的逻辑
  422. }
  423. });
  424. }
  425. Widget _buildProtocolAgreement() {
  426. return Row(
  427. mainAxisAlignment: MainAxisAlignment.center,
  428. crossAxisAlignment: CrossAxisAlignment.center,
  429. spacing: 1.w,
  430. children: [
  431. SizedBox(
  432. width: 32.w,
  433. height: 32.w,
  434. child: Checkbox(
  435. activeColor: color5F59F7,
  436. value: _agreeProtocol,
  437. onChanged: (value) {
  438. setState(() {
  439. _agreeProtocol = value ?? false;
  440. });
  441. },
  442. ),
  443. ),
  444. GestureDetector(
  445. onTap: () {
  446. context.push("/user/privacy");
  447. },
  448. child: Container(
  449. color: Colors.white,
  450. child: Text.rich(
  451. TextSpan(
  452. children: [
  453. TextSpan(
  454. text: _isLogin ? '登录即同意' : '注册即同意',
  455. style: TextStyle(fontSize: 11.sp),
  456. ),
  457. TextSpan(
  458. text: '《新华新消费服务协议》',
  459. style: TextStyle(color: color5F59F7, fontSize: 11.sp),
  460. ),
  461. TextSpan(text: '和', style: TextStyle(fontSize: 11.sp)),
  462. TextSpan(
  463. text: '《隐私政策》',
  464. style: TextStyle(color: color5F59F7, fontSize: 11.sp),
  465. ),
  466. ],
  467. ),
  468. ),
  469. ),
  470. ),
  471. ],
  472. );
  473. }
  474. bool isPhoneNumber(String input) {
  475. final RegExp phoneRegex = RegExp(r'^1[3-9]\d{9}$');
  476. return phoneRegex.hasMatch(input);
  477. }
  478. bool isPasswordValid(String password) {
  479. // 至少包含一个字母和一个数字,长度至少6位
  480. final RegExp passwordRegex = RegExp(
  481. r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$',
  482. );
  483. return passwordRegex.hasMatch(password);
  484. }
  485. void _submitForm(BuildContext context, WidgetRef ref) async {
  486. if (!_agreeProtocol) {
  487. showToast("请先同意服务协议");
  488. return;
  489. }
  490. String uid = _phoneController.text;
  491. String password = _passwordController.text;
  492. String password2 = _password2Controller.text;
  493. // String smsCode = _smsCodeController.text;
  494. if (uid.isEmpty || isPhoneNumber(uid) == false) {
  495. showToast("请输入手机号");
  496. return;
  497. }
  498. if (usePasswordLogin && password.isEmpty) {
  499. showToast("请输入密码");
  500. return;
  501. }
  502. if (!_isLogin) {
  503. if (password != password2) {
  504. showToast("密码不一致");
  505. return;
  506. }
  507. if (isPasswordValid(password) == false) {
  508. showToast("密码长度需大于8位,并包含字母和数字");
  509. return;
  510. }
  511. }
  512. if (!_isLogin) {
  513. //注册
  514. uuid = await HttpUtil().post(
  515. apiRegister,
  516. data: {"type": "phone", "phonenumber": uid, "password": password},
  517. );
  518. } else {
  519. //登录
  520. if (usePasswordLogin) {
  521. uuid = await HttpUtil().post(
  522. apiLogin,
  523. data: {"username": uid, "password": password},
  524. );
  525. } else {
  526. String smsCode = _smsCodeController.text;
  527. if (smsCode.isEmpty) {
  528. showToast("请输入验证码");
  529. return;
  530. }
  531. uuid = await HttpUtil().post(
  532. apiLoginSMSCode,
  533. data: {"mobile": uid, "code": smsCode},
  534. );
  535. }
  536. }
  537. if (uuid.isEmpty) {
  538. return;
  539. }
  540. saveUuid(uuid);
  541. ref.read(globalUserProvider.notifier).fetchUserInfo();
  542. if (context.mounted) context.go("/main");
  543. }
  544. }