基础库
 
 
 
 
 

632 lines
20 KiB

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