基础库
 
 
 
 
 

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