基础库
 
 
 
 
 

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