基础库
 
 
 
 
 

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