基础库
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 

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