基础库
 
 
 
 
 

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