基础库
 
 
 
 
 

621 lines
20 KiB

  1. import 'package:flutter/cupertino.dart';
  2. import 'package:flutter/gestures.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:cached_network_image/cached_network_image.dart';
  5. import 'package:flutter_bloc/flutter_bloc.dart';
  6. import 'package:provider/provider.dart';
  7. import 'package:zhiying_comm/pages/login_page/invite/login_invite_page.dart';
  8. import 'package:zhiying_comm/pages/login_page/model/login_model.dart';
  9. import 'package:zhiying_comm/zhiying_comm.dart';
  10. import 'bloc/bloc.dart';
  11. import 'bloc/login_account_repository.dart';
  12. import 'login_account_sk.dart';
  13. import 'widget/slide_verify_widget.dart';
  14. import 'package:zhiying_comm/util/empty_util.dart';
  15. import 'package:fluttertoast/fluttertoast.dart';
  16. import 'widget/vcode_widget.dart';
  17. ///
  18. /// 账号登陆(手机验证码,密码)
  19. ///
  20. class LoginAccountPage extends StatelessWidget {
  21. final Map<String, dynamic> model;
  22. const LoginAccountPage(this.model, {Key key}) : super(key: key);
  23. @override
  24. Widget build(BuildContext context) {
  25. return Scaffold(
  26. resizeToAvoidBottomInset: false,
  27. backgroundColor: HexColor.fromHex('#FFFFFF'),
  28. body: BlocProvider<LoginAccountBloc>(
  29. create: (_) => LoginAccountBloc(repository: LoginAccountRepository())..add(LoginAccountInitEvent()),
  30. child: LoginAccountPageContianer(),
  31. ),
  32. );
  33. }
  34. }
  35. /// 啦啦啦
  36. class LoginAccountPageContianer extends StatefulWidget {
  37. @override
  38. _LoginAccountPageContianerState createState() => _LoginAccountPageContianerState();
  39. }
  40. ///
  41. /// 主体逻辑
  42. ///
  43. class _LoginAccountPageContianerState extends State<LoginAccountPageContianer> implements OnClickListener {
  44. TextEditingController _phoneEdController;
  45. TextEditingController _vcodeEdController;
  46. TextEditingController _passEdController;
  47. FocusNode _phoneFN;
  48. FocusNode _passFN;
  49. FocusNode _vcodeFN;
  50. // 是否登录中
  51. bool _isLogging = false;
  52. /// 跳转到邀请码页面
  53. void _openInvitePage() {
  54. print('跳转到邀请码页面');
  55. RouterUtil.hideKeyboard(context);
  56. Navigator.push(
  57. context,
  58. CupertinoPageRoute(
  59. // builder: (_) => PageFactory.create('login_invite', null)
  60. builder: (_) => LoginInvitePage()));
  61. }
  62. /// 登陆成功页面
  63. void _openLoginSuccessPage() {
  64. RouterUtil.hideKeyboard(context);
  65. RouterUtil.goBackHomePage(context);
  66. // Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (BuildContext context) => PageFactory.create('homePage', null)), (Route<dynamic> route) => false,);
  67. }
  68. /// 返回上一页
  69. void _openPop() {
  70. if (Navigator.canPop(context)) {
  71. Navigator.pop(context);
  72. }
  73. }
  74. /// 登陆
  75. void _submitOnClick() {
  76. print('登陆');
  77. if (_checkParam(true) && !_isLogging) {
  78. setState(() {
  79. _isLogging = true;
  80. });
  81. if (_useVcode) {
  82. BlocProvider.of<LoginAccountBloc>(context).add(LoginAccountTypeVcodeEvent(mobile: _phoneEdController?.text?.toString()?.trim() ?? '', captcha: _vcodeEdController?.text?.toString()?.trim() ?? ''));
  83. } else {
  84. BlocProvider.of<LoginAccountBloc>(context).add(LoginAccountTypePasswordEvent(username: _phoneEdController?.text?.toString()?.trim() ?? '', password: _passEdController?.text?.toString()?.trim() ?? ''));
  85. }
  86. }else{
  87. Logger.log('参数有误 或者 正在登录中。。。');
  88. }
  89. }
  90. /// 切换登陆方式
  91. void _changeLoginTypeOnClick() {
  92. print('切换登陆');
  93. _passFN?.unfocus();
  94. _vcodeFN?.unfocus();
  95. _phoneFN?.unfocus();
  96. setState(() {
  97. _useVcode = !_useVcode;
  98. _canSubmit = false;
  99. });
  100. // 清除缓存
  101. if (_useVcode) {
  102. _passEdController?.clear();
  103. } else {
  104. _vcodeEdController?.clear();
  105. }
  106. }
  107. /// 同意协议
  108. void _agreeOnClick() {
  109. print('同意协议');
  110. setState(() {
  111. _acceptAgreement = !_acceptAgreement;
  112. });
  113. _checkParam(false);
  114. }
  115. /// 打开协议
  116. void _openAgreement(String url) {
  117. if (!EmptyUtil.isEmpty(url)) {
  118. print('打开协议$url');
  119. RouterUtil.openWebview(url, context);
  120. }
  121. }
  122. /// 输入框监听
  123. void _onChange(string) {
  124. print('$string');
  125. _checkParam(false);
  126. }
  127. /// 校验登陆参数
  128. bool _checkParam(bool needToast) {
  129. // 验证码
  130. if (_useVcode) {
  131. String phone = _phoneEdController?.text?.toString()?.trim() ?? null;
  132. String vcode = _vcodeEdController?.text?.toString()?.trim() ?? null;
  133. if (EmptyUtil.isEmpty(phone)) {
  134. if (needToast) Fluttertoast.showToast(msg: '手机号不能为空!');
  135. return false;
  136. }
  137. if (phone.length != 11) {
  138. if (needToast) Fluttertoast.showToast(msg: '手机号格式有误!');
  139. return false;
  140. }
  141. if (EmptyUtil.isEmpty(vcode)) {
  142. if (needToast) Fluttertoast.showToast(msg: '验证码不能为空!');
  143. return false;
  144. }
  145. if (vcode.length < 4) {
  146. if (needToast) Fluttertoast.showToast(msg: '验证码号格式有误!');
  147. return false;
  148. }
  149. } else {
  150. String phone = _phoneEdController?.text?.toString()?.trim() ?? null;
  151. String pass = _passEdController?.text?.toString()?.trim() ?? null;
  152. if (EmptyUtil.isEmpty(phone)) {
  153. if (needToast) Fluttertoast.showToast(msg: '手机号不能为空!');
  154. return false;
  155. }
  156. if (phone.length != 11) {
  157. if (needToast) Fluttertoast.showToast(msg: '手机号格式有误!');
  158. return false;
  159. }
  160. if (EmptyUtil.isEmpty(pass)) {
  161. if (needToast) Fluttertoast.showToast(msg: '验证码不能为空!');
  162. return false;
  163. }
  164. if (pass.length < 4) {
  165. if (needToast) Fluttertoast.showToast(msg: '验证码号格式有误!');
  166. return false;
  167. }
  168. }
  169. if (!_acceptAgreement) {
  170. if (needToast) Fluttertoast.showToast(msg: '请同意用户协议与隐私政策');
  171. return false;
  172. }
  173. setState(() {
  174. _canSubmit = true;
  175. });
  176. return true;
  177. }
  178. /// 检测手机号是否合法
  179. bool _checkPhoneNumParam(bool needToast) {
  180. String phone = _phoneEdController?.text?.toString()?.trim() ?? null;
  181. if (EmptyUtil.isEmpty(phone)) {
  182. if (needToast) Fluttertoast.showToast(msg: '手机号不能为空!');
  183. return false;
  184. }
  185. if (phone.length != 11) {
  186. if (needToast) Fluttertoast.showToast(msg: '手机号格式有误!');
  187. return false;
  188. }
  189. return true;
  190. }
  191. /// 是否使用验证码登陆 默认使用
  192. bool _useVcode = true;
  193. /// 是否可以登陆
  194. bool _canSubmit = false;
  195. /// 是否同意协议
  196. bool _acceptAgreement = true;
  197. /// 是否显示第三方验证码
  198. bool _showOtherVcode = false;
  199. @override
  200. void initState() {
  201. _phoneEdController = TextEditingController();
  202. _passEdController = TextEditingController();
  203. _vcodeEdController = TextEditingController();
  204. _vcodeFN = FocusNode();
  205. _passFN = FocusNode();
  206. _phoneFN = FocusNode();
  207. super.initState();
  208. }
  209. @override
  210. void dispose() {
  211. _phoneEdController?.dispose();
  212. _passEdController?.dispose();
  213. _vcodeEdController?.dispose();
  214. _phoneFN?.unfocus();
  215. _passFN?.unfocus();
  216. _vcodeFN?.unfocus();
  217. _phoneFN?.dispose();
  218. _passFN?.dispose();
  219. _vcodeFN?.dispose();
  220. super.dispose();
  221. }
  222. @override
  223. bool onVcodeClick() {
  224. /// 获取验证码
  225. if (_checkPhoneNumParam(true)) {
  226. BlocProvider.of<LoginAccountBloc>(context).add(LoginAccountGetVcodeEvent(mobile: _phoneEdController?.text?.toString()?.trim() ?? ''));
  227. return true;
  228. }
  229. return false;
  230. }
  231. @override
  232. Widget build(BuildContext context) {
  233. return BlocConsumer<LoginAccountBloc, LoginAccountState>(
  234. listener: (context, state) {
  235. if (state is LoginAccountLoginSuccessState) {}
  236. },
  237. buildWhen: (prev, current) {
  238. // 登陆失败
  239. if (current is LoginAccountLoginErrorState) {
  240. // Fluttertoast.showToast(msg: '登陆失败');
  241. setState(() {
  242. _isLogging = false;
  243. });
  244. return false;
  245. }
  246. // 登陆成功
  247. if (current is LoginAccountLoginSuccessState) {
  248. /// 缓存登陆数据
  249. Provider.of<UserInfoNotifier>(context, listen: false)?.setUserInfo(current.model);
  250. if (current?.model?.registerInviteCodeEnable != '1') {
  251. Fluttertoast.showToast(msg: '登录成功~');
  252. /// 打开也买
  253. _openLoginSuccessPage();
  254. } else {
  255. /// 打开邀请页面
  256. _openInvitePage();
  257. }
  258. return false;
  259. }
  260. // 获取验证码成功
  261. if (current is LoginAccountGetVcodeSuccessState) {
  262. Fluttertoast.showToast(msg: '验证码下发成功');
  263. return false;
  264. }
  265. // 获取验证码失败
  266. if (current is LoginAccountGetVcodeErrorState) {
  267. Fluttertoast.showToast(msg: '验证码获取失败~');
  268. return false;
  269. }
  270. return true;
  271. },
  272. builder: (context, state) {
  273. print('state = $state');
  274. if (state is LoginAccountLoadedState) {
  275. return _getMainWidget(state.model);
  276. }
  277. // 返回骨架屏
  278. return LoginAccountSkeleton();
  279. },
  280. );
  281. }
  282. /// 主页面
  283. Widget _getMainWidget(LoginModel model) {
  284. print(model);
  285. return Column(
  286. children: <Widget>[
  287. /// appBar
  288. _getAppBarWidget(model),
  289. /// title
  290. Padding(padding: const EdgeInsets.only(left: 27.5, right: 27.5, top: 40), child: _getTitleWidget(model)),
  291. /// 手机输入框
  292. Padding(padding: const EdgeInsets.only(left: 27.5, right: 27.5, top: 30), child: _getPhoneWidget(model)),
  293. /// 验证码
  294. Visibility(visible: _useVcode, child: Padding(padding: const EdgeInsets.only(left: 27.5, right: 27.5, top: 15), child: _getVcodeWidget(model))),
  295. /// 密码
  296. Visibility(visible: !_useVcode, child: Padding(padding: const EdgeInsets.only(left: 27.5, right: 27.5, top: 15), child: _getPassInputWidget(model))),
  297. /// 第三方验证码
  298. Visibility(visible: _showOtherVcode, child: Padding(padding: const EdgeInsets.only(left: 27.5, right: 27.5, top: 15), child: _getOtherVcodeInputWidget(model))),
  299. /// 切换登陆方式tip
  300. Padding(padding: const EdgeInsets.only(top: 15), child: _getChangeTipWidget(model)),
  301. /// 登录按钮
  302. Padding(padding: const EdgeInsets.only(left: 27.5, right: 27.5, top: 30), child: _getSubmiBtnWidget(model)),
  303. /// 协议
  304. Padding(padding: const EdgeInsets.only(top: 15), child: _getProtoclWidget(model)),
  305. /// 底部提示tip
  306. Visibility(visible: _useVcode, child: Expanded(child: Align(alignment: Alignment.bottomCenter, child: _getBottomTipWidget(model))))
  307. ],
  308. );
  309. }
  310. /// appBar
  311. Widget _getAppBarWidget(LoginModel model) {
  312. return AppBar(
  313. backgroundColor: HexColor.fromHex('#FFFFFF'),
  314. brightness: Brightness.light,
  315. elevation: 0,
  316. title: Text(
  317. model?.mobile?.appBarTitle ?? '登录',
  318. style: TextStyle(color: HexColor.fromHex(model?.mobile?.appBarTitleColor ?? '#333333')),
  319. ),
  320. centerTitle: true,
  321. leading: IconButton(
  322. icon: Icon(
  323. Icons.arrow_back_ios,
  324. size: 22,
  325. color: HexColor.fromHex('#333333'),
  326. ),
  327. onPressed: () => _openPop(),
  328. ),
  329. );
  330. }
  331. /// title
  332. Widget _getTitleWidget(LoginModel model) {
  333. return Align(
  334. alignment: Alignment.centerLeft,
  335. child: Text(
  336. model?.mobile?.title ?? '您好,欢迎登录',
  337. style: TextStyle(color: HexColor.fromHex(model?.mobile?.titleColor ?? '#333333'), fontSize: 25),
  338. ));
  339. }
  340. /// 手机输入框
  341. Widget _getPhoneWidget(LoginModel model) {
  342. return _getCustomInputWidget(
  343. hint: model?.mobile?.inputMobileHintText ?? '请输入您的手机号',
  344. controller: _phoneEdController,
  345. focusNode: _phoneFN,
  346. onChanged: _onChange,
  347. hintColor: model?.mobile?.inputHintColor ?? '#999999',
  348. bgColor: model?.mobile?.inputBgColor ?? '#F7F7F7',
  349. textColor: model?.mobile?.inputTextColor ?? '#333333',
  350. iconUrl: model?.mobile?.inputMobileIcon ?? '');
  351. }
  352. /// 验证码输入框
  353. Widget _getVcodeWidget(LoginModel model) {
  354. return Container(
  355. height: 42,
  356. child: Stack(
  357. children: <Widget>[
  358. _getCustomInputWidget(
  359. controller: _vcodeEdController,
  360. focusNode: _vcodeFN,
  361. onChanged: _onChange,
  362. hintColor: model?.mobile?.inputHintColor ?? '#999999',
  363. hint: model?.mobile?.inputVcodeHintText ?? '请输入您的验证码',
  364. bgColor: model?.mobile?.inputBgColor ?? '#F7F7F7',
  365. textColor: model?.mobile?.inputTextColor ?? '#333333',
  366. iconUrl: model?.mobile?.inputVcodeIcon ?? ''),
  367. Align(alignment: Alignment.centerRight, child: _getVcodeButtonWidget(model)),
  368. ],
  369. ),
  370. );
  371. }
  372. /// 验证码按钮
  373. Widget _getVcodeButtonWidget(LoginModel model) {
  374. return VcodeWidget(
  375. onCallBack: this,
  376. awaitTime: int.parse(model?.mobile?.vcodeTime ?? '60'),
  377. btnAwaitText: '秒',
  378. btnText: '获取验证码',
  379. btnTextColor: model?.mobile?.btnVcodeTextColor ?? '#FFFFFF',
  380. color: model?.mobile?.btnVcodeBgColor ?? '#FF4343',
  381. disabledColor: model?.mobile?.btnVcodeBanBgColor ?? '#DDDDDD',
  382. disabledTextColor: model?.mobile?.btnVcodeBanTextColor ?? '#FFFFFF');
  383. }
  384. /// 第三方验证码输入框
  385. Widget _getOtherVcodeInputWidget(var model) {
  386. return Container(
  387. width: 240,
  388. height: 42,
  389. alignment: Alignment.centerLeft,
  390. child: SlideVerifyWidget(
  391. width: 240,
  392. ),
  393. // child: Row(
  394. // children: <Widget>[
  395. // // 输入框
  396. // Expanded(
  397. // child: _getCustomInputWidget(hint: '请输入右方验证码', hintColor: '#999999', textColor: '#333333', bgColor: '#F7F7F7', iconUrl: null, )
  398. // ),
  399. // // 第三方验证码
  400. // Container(
  401. // margin: const EdgeInsets.only(left: 5),
  402. // width: 100,
  403. // height: double.infinity,
  404. // decoration: BoxDecoration(
  405. // borderRadius: BorderRadius.circular(8),
  406. // color: Colors.red
  407. // ),
  408. // ),
  409. //
  410. // ],
  411. // ),
  412. );
  413. }
  414. /// 密码输入框
  415. Widget _getPassInputWidget(LoginModel model) {
  416. return Container(
  417. height: 42,
  418. child: _getCustomInputWidget(
  419. obscureText: true,
  420. keyboardType: TextInputType.text,
  421. controller: _passEdController,
  422. focusNode: _passFN,
  423. onChanged: _onChange,
  424. hint: model?.mobile?.inputPassHintText ?? '请输入您的密码',
  425. iconUrl: model?.mobile?.inputPassIcon ?? '',
  426. hintColor: model?.mobile?.inputHintColor ?? '#999999',
  427. textColor: model?.mobile?.inputTextColor ?? '#333333',
  428. bgColor: model?.mobile?.inputBgColor ?? '#F7F7F7'),
  429. );
  430. }
  431. /// 切换登陆方式tip
  432. Widget _getChangeTipWidget(LoginModel model) {
  433. return GestureDetector(
  434. behavior: HitTestBehavior.opaque,
  435. onTap: () => _changeLoginTypeOnClick(),
  436. child: Text(
  437. _useVcode ? model?.mobile?.textUsePassTip ?? '使用密码登录' : model?.mobile?.textUseVcodeTip ?? '使用验证码登陆',
  438. style: TextStyle(fontSize: 12, color: HexColor.fromHex(_useVcode ? model?.mobile?.textUsePassTipColor ?? '#999999' : model?.mobile?.textUseVcodeTipColor ?? '#999999')),
  439. ),
  440. );
  441. }
  442. /// 登陆按钮
  443. Widget _getSubmiBtnWidget(LoginModel model) {
  444. return Material(
  445. child: Container(
  446. height: 52,
  447. width: double.infinity,
  448. color: Colors.white,
  449. child: RaisedButton(
  450. child: Text(
  451. _isLogging ? '登录中...' : model?.mobile?.btnLoginText ?? '立即登录',
  452. style: TextStyle(fontSize: 15),
  453. ),
  454. textColor: HexColor.fromHex(model?.mobile?.btnLoginTextColor ?? '#FFFFFF'),
  455. color: HexColor.fromHex(model?.mobile?.btnLoginBgColor ?? '#FF3939'),
  456. disabledColor: HexColor.fromHex(model?.mobile?.btnLoginBanBgColor ?? '#F5F5F5'),
  457. disabledTextColor: HexColor.fromHex(model?.mobile?.btnLoginBanTextColor ?? '#999999'),
  458. elevation: 5,
  459. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(52 / 2)),
  460. onPressed: _canSubmit ? _submitOnClick : null,
  461. ),
  462. ),
  463. );
  464. }
  465. /// 协议
  466. Widget _getProtoclWidget(LoginModel model) {
  467. // return Text('同意《嗨如意用户协议》 及《营私政策》', style: TextStyle(fontSize: 11, color: HexColor.fromHex('#C0C0C0')));
  468. return Row(
  469. mainAxisAlignment: MainAxisAlignment.center,
  470. children: <Widget>[
  471. /// 图标
  472. GestureDetector(
  473. behavior: HitTestBehavior.opaque,
  474. onTap: () => _agreeOnClick(),
  475. child: Padding(
  476. padding: const EdgeInsets.all(8.0),
  477. child: CachedNetworkImage(
  478. imageUrl: _acceptAgreement ? model?.mobile?.protocolSelectIcon ?? '' : model?.mobile?.protocolUnselectIcon ?? '',
  479. width: 12,
  480. ))),
  481. /// 协议文字
  482. RichText(
  483. text: TextSpan(
  484. text: '',
  485. children: model.mobile.protocol.map((item) {
  486. return TextSpan(
  487. text: item?.text,
  488. style: TextStyle(color: HexColor.fromHex(item?.textColor), fontSize: 10),
  489. recognizer: TapGestureRecognizer()
  490. ..onTap = () {
  491. _openAgreement(item.url);
  492. });
  493. }).toList()),
  494. )
  495. ],
  496. );
  497. }
  498. /// 底部提示tip
  499. Widget _getBottomTipWidget(LoginModel model) {
  500. return Padding(
  501. padding: const EdgeInsets.only(bottom: 25),
  502. child: Text(
  503. model?.mobile?.textBottomTip ?? '未注册过的手机将自动注册',
  504. style: TextStyle(fontSize: 11, color: HexColor.fromHex(model?.mobile?.textBottomTipColor ?? '#999999')),
  505. ),
  506. );
  507. }
  508. /// 自定义输入框
  509. Widget _getCustomInputWidget({
  510. String hint,
  511. String hintColor,
  512. String bgColor,
  513. String textColor,
  514. String iconUrl,
  515. TextEditingController controller,
  516. ValueChanged<String> onChanged,
  517. FocusNode focusNode,
  518. TextInputType keyboardType,
  519. bool obscureText = false,
  520. }) {
  521. var border = OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: HexColor.fromHex(bgColor), width: 0));
  522. return Container(
  523. height: 42,
  524. padding: const EdgeInsets.symmetric(horizontal: 15),
  525. decoration: BoxDecoration(
  526. color: HexColor.fromHex(bgColor),
  527. borderRadius: BorderRadius.circular(8),
  528. ),
  529. child: Row(
  530. mainAxisAlignment: MainAxisAlignment.start,
  531. crossAxisAlignment: CrossAxisAlignment.center,
  532. children: <Widget>[
  533. CachedNetworkImage(
  534. imageUrl: iconUrl ?? '',
  535. width: 10,
  536. ),
  537. Expanded(
  538. child: TextField(
  539. obscureText: obscureText ?? false,
  540. controller: controller,
  541. focusNode: focusNode,
  542. onChanged: onChanged,
  543. expands: false,
  544. style: TextStyle(color: HexColor.fromHex(textColor)),
  545. maxLines: 1,
  546. keyboardType: keyboardType ?? TextInputType.number,
  547. decoration: InputDecoration(
  548. contentPadding: EdgeInsets.only(top: 30, left: 7.5),
  549. hintText: hint,
  550. hintStyle: TextStyle(fontSize: 13, color: HexColor.fromHex(hintColor)),
  551. hintMaxLines: 1,
  552. filled: true,
  553. fillColor: Colors.transparent,
  554. border: border,
  555. focusedBorder: border,
  556. enabledBorder: border,
  557. disabledBorder: border,
  558. errorBorder: border,
  559. focusedErrorBorder: border,
  560. ),
  561. ),
  562. ),
  563. ],
  564. ),
  565. );
  566. }
  567. }