基础库
 
 
 
 
 

592 lines
19 KiB

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